From d1da6a01cc1c494e81aa30033b05d672fda18fd1 Mon Sep 17 00:00:00 2001 From: Faraaz Biyabani Date: Fri, 12 Jan 2024 16:15:01 +0530 Subject: [PATCH 1/8] feat: determine LP reference and rte paths during query resolution --- package-lock.json | 11 + package.json | 1 + src/create-resolvers.js | 28 ++ src/live-preview/getCslpMetaPaths.js | 120 ++++++++ src/live-preview/index.js | 392 +++++++-------------------- src/live-preview/resolveCslpMeta.js | 93 +++++++ 6 files changed, 344 insertions(+), 301 deletions(-) create mode 100644 src/live-preview/getCslpMetaPaths.js create mode 100644 src/live-preview/resolveCslpMeta.js diff --git a/package-lock.json b/package-lock.json index eb5a666..ca576ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "contentstack": "^3.17.1", "gatsby-core-utils": "^3.23.0", "gatsby-source-filesystem": "^5.7.0", + "lodash.clonedeep": "^4.5.0", "lodash.isempty": "^4.4.0", "node-fetch": "^2.6.9", "progress": "^2.0.3", @@ -10225,6 +10226,11 @@ "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", "dev": true }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -23838,6 +23844,11 @@ "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", "dev": true }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", diff --git a/package.json b/package.json index cfa0504..463275e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "contentstack": "^3.17.1", "gatsby-core-utils": "^3.23.0", "gatsby-source-filesystem": "^5.7.0", + "lodash.clonedeep": "^4.5.0", "lodash.isempty": "^4.4.0", "node-fetch": "^2.6.9", "progress": "^2.0.3", diff --git a/src/create-resolvers.js b/src/create-resolvers.js index 4c6f65e..57c3032 100644 --- a/src/create-resolvers.js +++ b/src/create-resolvers.js @@ -4,6 +4,7 @@ const Contentstack = require('@contentstack/utils'); const { getJSONToHtmlRequired } = require('./utils'); const { makeEntryNodeUid, makeAssetNodeUid } = require('./normalize'); +const { resolveCslpMeta } = require('./live-preview/resolveCslpMeta'); exports.createResolvers = async ({ createResolvers, cache, createNodeId }, configOptions) => { const resolvers = {}; @@ -16,6 +17,33 @@ exports.createResolvers = async ({ createResolvers, cache, createNodeId }, confi cache.get(`${typePrefix}_${configOptions.api_key}_json_rte_fields`), ]); + const contentTypes = await cache.get(typePrefix); + const contentTypeMap = {}; + contentTypes.forEach((item) => { + contentTypeMap[item.uid] = item; + }); + + contentTypes.forEach((contentType) => { + resolvers[`${typePrefix}_${contentType.uid}`] = { + "cslp__meta": { + type: "JSON", + resolve(source, args, context, info) { + try { + return resolveCslpMeta({ source, args, context, info, contentTypeMap, typePrefix }) + } + catch (error) { + console.error("ContentstackGatsby (Live Preview):", error) + return { + error: { + message: error.message ?? "failed to resolve cslp__meta" + } + } + } + } + } + } + }) + fileFields && fileFields.forEach(fileField => { resolvers[fileField.parent] = { ...resolvers[fileField.parent], diff --git a/src/live-preview/getCslpMetaPaths.js b/src/live-preview/getCslpMetaPaths.js new file mode 100644 index 0000000..5dfba50 --- /dev/null +++ b/src/live-preview/getCslpMetaPaths.js @@ -0,0 +1,120 @@ +export function getCslpMetaPaths( + selectionSet, + path = "", + schema, + fragments, + contentTypeMap, + typePrefix +) { + const paths = { + referencePaths: [], + jsonRtePaths: [], + } + const refPaths = paths.referencePaths; + const rtePaths = paths.jsonRtePaths; + if (schema.data_type === "reference" && path) { + refPaths.push(path) + } + if (schema.data_type === "json" && schema.field_metadata?.allow_json_rte) { + rtePaths.push(path) + } + if (!selectionSet || !selectionSet.selections) { + return paths; + } + for (const selection of selectionSet.selections) { + // exit when selection.kind is not Field, SelectionSet or InlineFragment + // selection.name is not present for selection.kind is "InlineFragment" + if (selection?.name?.value === "cslp__meta") { + continue; + } + if ( + selection.selectionSet || + selection.kind === "Field" || + selection.kind === "InlineFragment" || + selection.kind === "FragmentSpread" + ) { + + const fragmentName = selection.name?.value; + const fragmentDefinition = fragments[fragmentName]; + const inlineFragmentNodeType = selection.typeCondition?.name?.value; + + // Fragment + // note - when a fragment is used inside a reference field, the reference field + // path gets added twice, this can maybe avoided by re-structuring the code, + // but a Set works fine + if (selection.kind === "FragmentSpread" && fragmentDefinition) { + const fragmentSpreadPaths = getCslpMetaPaths( + fragmentDefinition.selectionSet, + path, + schema, + fragments, + contentTypeMap, + typePrefix + ); + combineCslpMetaPaths(fragmentSpreadPaths, paths) + } + // InlineFragment (ref_multiple) + else if (selection.kind === "InlineFragment" && inlineFragmentNodeType) { + const contentTypeUid = inlineFragmentNodeType.replace(`${typePrefix}_`, ""); + if (!contentTypeUid || !(contentTypeUid in contentTypeMap)) { + return paths; + } + const contentTypeSchema = contentTypeMap[contentTypeUid]; + const inlineFragmentPaths = getCslpMetaPaths( + selection.selectionSet, + path, + contentTypeSchema, + fragments, + contentTypeMap, + typePrefix + ); + combineCslpMetaPaths(inlineFragmentPaths, paths) + } + // SelectionSet (all fields that can have nested properties) + else { + let nestedFields = schema?.blocks ?? schema.schema; + // cannot traverse inside file or link schema + if (schema.data_type === "file" || schema.data_type === "link") { + return paths; + } + // when a reference, change nested fields to schema of referenced CT + if (schema.data_type === "reference" && schema.reference_to) { + nestedFields = contentTypeMap[schema.reference_to[0]].schema + } + const nestedFieldSchema = nestedFields.find((item) => item.uid === selection.name.value); + if (nestedFieldSchema) { + let nextPath = []; + if (path) { + nextPath = path.split("."); + } + nextPath.push(selection.name.value); + const metaPaths = getCslpMetaPaths( + selection.selectionSet, + nextPath.join("."), + nestedFieldSchema, + fragments, + contentTypeMap, + typePrefix + ); + combineCslpMetaPaths(metaPaths, paths); + } + } + } + } + return { + referencePaths: Array.from(new Set(paths.referencePaths)), + jsonRtePaths: Array.from(new Set(paths.jsonRtePaths)) + }; +} + +/** + * @typedef {{referencePaths: string[], jsonRtePaths: string[]}} paths + * @param {paths} source + * @param {paths} target + * @returns merges the path fields from the source into the target's path fields + */ +function combineCslpMetaPaths(source, target) { + target.referencePaths.push(...source.referencePaths); + target.jsonRtePaths.push(...source.jsonRtePaths); + return target; +} diff --git a/src/live-preview/index.js b/src/live-preview/index.js index 3c54d15..6cdb787 100644 --- a/src/live-preview/index.js +++ b/src/live-preview/index.js @@ -1,10 +1,8 @@ -import contentstack from 'contentstack'; -import { jsonToHTML } from '@contentstack/utils'; -import isEmpty from 'lodash.isempty'; -import { Storage } from './storage-helper'; - -// max depth for nested references -const MAX_DEPTH_ALLOWED = 5; +import contentstack from "contentstack"; +import { jsonToHTML } from "@contentstack/utils"; +import isEmpty from "lodash.isempty"; +import cloneDeep from "lodash.clonedeep"; +import { Storage } from "./storage-helper"; export class ContentstackGatsby { config; @@ -35,190 +33,30 @@ export class ContentstackGatsby { } this.stackSdk = contentstack.Stack(stackConfig); - // reference fields in various CTs and the CTs they refer - this.referenceFieldsStorage = new Storage( - window.sessionStorage, - 'reference_fields' - ); - this.referenceFields = this.referenceFieldsStorage.get(); - this.statusStorage = new Storage(window.sessionStorage, "status") - - // json rte fields in various CTs - this.jsonRteFieldsStorage = new Storage( - window.sessionStorage, - 'json_rte_fields' - ); - this.jsonRteFields = this.jsonRteFieldsStorage.get(); - - // only field paths extracted from the above map for current CT - this.referenceFieldPaths = []; - - // store content types in LP site's session storage - this.contentTypesStorage = new Storage( - window.sessionStorage, - 'content_types' - ); - this.contentTypes = this.contentTypesStorage.get(); } setHost(host) { this.stackSdk.setHost(host); } - static addContentTypeUidFromTypename(entry) { - if (typeof entry === "undefined") { - throw new TypeError("entry cannot be empty"); - } - if (entry === null) { - throw new TypeError("entry cannot be null") - } - if (typeof entry !== "object") { - throw new TypeError("entry must be an object") - } - if (Array.isArray(entry)) { - throw new TypeError("entry cannot be an object, pass an instance of entry") - } - - traverse(entry) - - function traverse(field) { - if (!field || typeof field !== "object") { - return; - } - if (Array.isArray(field)) { - field.forEach((instance) => traverse(instance)) - } - if (Object.hasOwnProperty.call(field, "__typename") && typeof field.__typename == "string") { - field._content_type_uid = field.__typename.split("_").slice(1).join("_"); - } - Object.values(field).forEach((subField) => traverse(subField)) - } - } - - async fetchContentTypes(uids) { - try { - const result = await this.stackSdk.getContentTypes({ - query: { uid: { $in: uids } }, - }); - if (result) { - const contentTypes = {}; - result.content_types.forEach(ct => { - contentTypes[ct.uid] = ct; + async fetchEntry(entryUid, contentTypeUid, referencePaths = [], jsonRtePaths = []) { + const entry = await this.stackSdk + .ContentType(contentTypeUid) + .Entry(entryUid) + .includeReference(referencePaths) + .toJSON() + .fetch(); + + if (!isEmpty(entry)) { + if (this.config.jsonRteToHtml) { + jsonToHTML({ + entry: entry, + paths: jsonRtePaths, }); - return contentTypes; - } - } catch (error) { - console.error('ContentstackGatsby - Failed to fetch content types'); - throw error; - } - } - - async getContentTypes(uids) { - // fetch and filter only content types that are not available in cache - const uidsToFetch = uids.filter(uid => !this.contentTypes[uid]); - if (!uidsToFetch.length) { - return this.contentTypes; - } - const types = await this.fetchContentTypes(uidsToFetch); - uidsToFetch.forEach(uid => { - // TODO need to set it in two places, can be better - this.contentTypes[uid] = types[uid]; - this.contentTypesStorage.set(uid, types[uid]); - }); - return this.contentTypes; - } - - async extractReferences( - refPathMap = {}, - status, - jsonRtePaths = [], - depth = MAX_DEPTH_ALLOWED, - seen = [] - ) { - if (depth <= 0) { - return refPathMap; - } - const uids = [...new Set(Object.values(refPathMap).flat())]; - const contentTypes = await this.getContentTypes(uids); - const refPathsCount = Object.keys(refPathMap).length; - const explorePaths = Object.entries(refPathMap).filter( - ([path]) => !seen.includes(path) - ); - for (const [refPath, refUids] of explorePaths) { - // mark this reference path as seen - seen.push(refPath); - for (const uid of refUids) { - let rPath = refPath.split('.'); - // when path is root, set path to [] - if (refPath === '') { - rPath = []; - } - - if (!status.hasLivePreviewEntryFound) { - status.hasLivePreviewEntryFound = this.isCurrentEntryEdited(uid) - } - - this.extractUids( - contentTypes[uid].schema, - rPath, - refPathMap, - jsonRtePaths - ); } + return entry; } - if (Object.keys(refPathMap).length > refPathsCount) { - await this.extractReferences(refPathMap, status, jsonRtePaths, depth - 1, seen); - } - return { refPathMap, jsonRtePaths }; - } - - extractUids(schema, pathPrefix = [], refPathMap = {}, jsonRtePaths = []) { - const referredUids = []; - for (const field of schema) { - const fieldPath = [...pathPrefix, field.uid]; - if ( - field.data_type === 'reference' && - Array.isArray(field.reference_to) && - field.reference_to.length > 0 - ) { - referredUids.push(...field.reference_to); - refPathMap[fieldPath.join('.')] = field.reference_to; - } else if ( - field.data_type === 'blocks' && - field.blocks && - field.blocks.length > 0 - ) { - for (const block of field.blocks) { - const { referredUids: blockRefUids } = this.extractUids( - block.schema, - [...fieldPath, block.uid], - refPathMap, - jsonRtePaths - ); - referredUids.push(...blockRefUids); - } - } else if ( - field.data_type === 'group' && - field.schema && - field.schema.length > 0 - ) { - const { referredUids: groupRefUids } = this.extractUids( - field.schema, - [...fieldPath], - refPathMap, - jsonRtePaths - ); - referredUids.push(...groupRefUids); - } else if ( - field.data_type === 'json' && - field.field_metadata?.allow_json_rte - ) { - const rtePath = [...pathPrefix, field.uid].join('.'); - jsonRtePaths.push(rtePath); - } - } - return { referredUids, refPathMap }; } isNested(value) { @@ -228,143 +66,95 @@ export class ContentstackGatsby { return false; } - /** - * Identify reference paths in user-provided data - * @param {any} data - entry data - * @param {string[]} currentPath - traversal path - * @param {string[]} referenceFieldPaths - content type reference paths - */ - identifyReferences(data, currentPath = [], referenceFieldPaths = []) { - const paths = []; - - for (const [k, v] of Object.entries(data)) { - if (!v) { - continue; - } - if (currentPath.length > 0) { - const refPath = currentPath.join('.'); - // if a reference path and not already collected, collect it - if (referenceFieldPaths.includes(refPath) && !paths.includes(refPath)) { - paths.push(refPath); - } - } - if (this.isNested(v)) { - const tempPath = [...currentPath]; - tempPath.push(k); - const p = this.identifyReferences(v, tempPath, referenceFieldPaths); - paths.push(...p); - } else if (Array.isArray(v)) { - const tempPath = [...currentPath]; - tempPath.push(k); - if (v.length > 0) { - // need to go over all refs since each of them could be of different - // content type and might contain refs - for (const val of v) { - const p = this.identifyReferences( - val, - tempPath, - referenceFieldPaths - ); - paths.push(...p); - } - } - // no multiple ref present, Gatsby value -> [] (empty array) - // no single ref present, Gatsby value -> [] - else if ( - v.length === 0 && - referenceFieldPaths.includes(tempPath.join('.')) - ) { - // it is a single or multiple ref - // also no idea what child references the user must be querying - // so need to get all child refs - paths.push(tempPath.join('.')); - const childRefPaths = referenceFieldPaths.filter(path => - path.startsWith(tempPath) - ); - paths.push(...childRefPaths); - } - } + unwrapEntryData(data) { + const values = Object.values(data); + if (!values) { + return data; } - return [...new Set(paths)]; - } - - isCurrentEntryEdited(entryContentType) { - return entryContentType === this.livePreviewConfig?.content_type_uid + if (values && values.length === 1) { + return values[0]; + } + return values; } async get(data) { - const receivedData = structuredClone(data); + if (data === null) { + console.warn("Contentstack Gatsby (Live Preview): null was passed to get()"); + return data; + } + if (!this.isNested(data)) { + console.warn("Contentstack Gatsby (Live Preview): data passed to get() is invalid") + return data; + } + const dataCloned = cloneDeep(data); + console.log(dataCloned) + delete dataCloned["$"]; + + const hasCslpMetaAtRoot = dataCloned.cslp__meta; + const multipleEntriesKey = Object.keys(dataCloned); + const hasSingleEntry = !hasCslpMetaAtRoot && multipleEntriesKey.length === 1; + const hasMultipleEntries = !hasCslpMetaAtRoot && multipleEntriesKey.length > 1; + + const receivedData = hasSingleEntry || hasMultipleEntries ? + this.unwrapEntryData(dataCloned) : + dataCloned; const { live_preview } = this.stackSdk; - let status = { - hasLivePreviewEntryFound: false - } + if (live_preview?.hash && live_preview.hash !== 'init') { this.livePreviewConfig = live_preview; - if (!receivedData.__typename) { - throw new Error("Entry data must contain __typename for live preview") - } - if (!receivedData.uid) { - throw new Error("Entry data must contain uid for live preview") - } - const contentTypeUid = receivedData.__typename.split("_").slice(1).join("_") - const entryUid = receivedData.uid; - - status = this.statusStorage.get(contentTypeUid) ?? { hasLivePreviewEntryFound: this.isCurrentEntryEdited(contentTypeUid) } - - if ( - isEmpty(this.referenceFields[contentTypeUid]) || - isEmpty(this.jsonRteFields[contentTypeUid]) - ) { - const { refPathMap, jsonRtePaths } = await this.extractReferences({ - '': [contentTypeUid], - }, status); - // store reference paths - this.referenceFields[contentTypeUid] = refPathMap; - this.referenceFieldsStorage.set( - contentTypeUid, - this.referenceFields[contentTypeUid] - ); - // store json rte paths - this.jsonRteFields[contentTypeUid] = jsonRtePaths; - this.jsonRteFieldsStorage.set( - contentTypeUid, - this.jsonRteFields[contentTypeUid] - ); - } - - let referencePaths = Object.keys(this.referenceFields[contentTypeUid]); - referencePaths = referencePaths.filter(field => !!field) - const paths = this.identifyReferences( - receivedData, - [], - referencePaths - ); + const hasCslpMeta = receivedData.cslp__meta; - this.statusStorage.set(contentTypeUid, status) + // item can be null, since gatsby can return null + const hasMultipleCslpMeta = Array.isArray(receivedData) && + receivedData.length > 1 && + receivedData.every((item) => item === null || item.cslp__meta); - if (!status.hasLivePreviewEntryFound) { - return receivedData + if (!hasCslpMeta && !hasMultipleCslpMeta) { + throw new Error("Contentstack Gatsby (Live Preview): Entry data must contain cslp__meta for live preview") } - const entry = await this.stackSdk - .ContentType(contentTypeUid) - .Entry(entryUid) - .includeReference(paths) - .toJSON() - .fetch(); - if (!isEmpty(entry)) { - if (this.config.jsonRteToHtml) { - jsonToHTML({ - entry: entry, - paths: this.jsonRteFields[contentTypeUid] ?? [], + if (hasMultipleCslpMeta) { + try { + const multipleLPEntries = await Promise.all(receivedData.map((item) => { + if (item === null) { + return Promise.resolve(null) + } + return this.fetchEntry( + item.cslp__meta.entryUid, + item.cslp__meta.contentTypeUid, + item.cslp__meta.refPaths, + item.cslp__meta.rtePaths + ) + })) + const result = {} + multipleEntriesKey.forEach((key, index) => { + result[key] = multipleLPEntries[index]; }); + return result; + } + catch (error) { + console.error("Contentstack Gatsby (Live Preview):", error); + return dataCloned; } - return entry; } + + const entryCslpMeta = receivedData.cslp__meta; + const contentTypeUid = entryCslpMeta.contentTypeUid; + const entryUid = entryCslpMeta.entryUid; + + if (!entryUid || !contentTypeUid) { + console.warn("Contentstack Gatsby (Live Preview): no entry uid or content type uid was found inside cslp__meta") + return dataCloned; + } + + const refPaths = entryCslpMeta.referencePaths ?? []; + const rtePaths = entryCslpMeta.jsonRtePaths ?? []; + const entry = await this.fetchEntry(entryUid, contentTypeUid, refPaths, rtePaths); + return entry; } - return receivedData; + return dataCloned; } } diff --git a/src/live-preview/resolveCslpMeta.js b/src/live-preview/resolveCslpMeta.js new file mode 100644 index 0000000..90cae36 --- /dev/null +++ b/src/live-preview/resolveCslpMeta.js @@ -0,0 +1,93 @@ +import { getCslpMetaPaths } from "./getCslpMetaPaths"; + +export function resolveCslpMeta({ + source, + args: _args, + context: _context, + info, + contentTypeMap, + typePrefix +}) { + const entryUid = source.uid; + const contentTypeNodeType = source.internal.type + const queryContentTypeUid = contentTypeNodeType.replace(`${typePrefix}_`, "") + + const fieldNode = info.fieldNodes.find((node) => node.name?.value === "cslp__meta"); + const fieldNodeValue = "cslp__meta"; + const fieldNodeLocation = { start: fieldNode.name?.loc?.start, end: fieldNode.name?.loc?.end } + + // We have all the query selections (`info.operation.selectionSet.selections`) + // each time we resolve cslp__meta. + // So, get the correct Selection from query for the current cslp__meta + const queryContentTypeSelection = findQuerySelection( + info.operation.selectionSet, + fieldNodeValue, + fieldNodeLocation + ) + + if (typeof queryContentTypeSelection === "undefined") { + return { + error: { + message: "failed to find query selection for cslp__meta" + } + } + } + + const fragments = info?.fragments ?? {}; + const contentType = contentTypeMap[queryContentTypeUid]; + // for the content type selection, get the reference and json RTE paths + const metaPaths = getCslpMetaPaths( + queryContentTypeSelection, + "", + contentType, + fragments, + contentTypeMap, + typePrefix + ) + const result = { + entryUid, + contentTypeUid: contentType.uid, + ...metaPaths, + } + return result; +} + +function findQuerySelection(selectionSet, value, location, depth = 0) { + // cslp__meta can only be one level deep (or two level deep, see all* case below) + // e.g. + // query { + // page { + // cslp__meta + // } + // allBlog { + // nodes { + // cslp__meta + // } + // } + // } + if (depth > 1 || !selectionSet || !selectionSet.selections) { + return; + } + for (const selection of selectionSet.selections) { + if ( + selection.name?.value === value && + selection.loc?.start === location.start && + selection.loc?.end === location.end + ) { + return selectionSet + } + // "nodes" in all* queries will lead to cslp__meta at depth 2 + if (selection.name?.value === "nodes") { + const nestedSelectionSet = findQuerySelection(selection.selectionSet, value, location, depth); + if (nestedSelectionSet) { + return nestedSelectionSet; + } + } + // search one level deeper for the correct node in this selection + const nestedSelectionSet = findQuerySelection(selection.selectionSet, value, location, depth + 1) + // return when not undefined, meaning the correct selection has been found + if (nestedSelectionSet) { + return nestedSelectionSet; + } + } +} \ No newline at end of file From c769da9aa3bfb7ca49e330d119cf7988af51750f Mon Sep 17 00:00:00 2001 From: Faraaz Biyabani Date: Fri, 12 Jan 2024 17:54:47 +0530 Subject: [PATCH 2/8] chore: remove log --- src/live-preview/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/live-preview/index.js b/src/live-preview/index.js index 6cdb787..1c7ab82 100644 --- a/src/live-preview/index.js +++ b/src/live-preview/index.js @@ -87,7 +87,6 @@ export class ContentstackGatsby { return data; } const dataCloned = cloneDeep(data); - console.log(dataCloned) delete dataCloned["$"]; const hasCslpMetaAtRoot = dataCloned.cslp__meta; @@ -100,8 +99,6 @@ export class ContentstackGatsby { dataCloned; const { live_preview } = this.stackSdk; - - if (live_preview?.hash && live_preview.hash !== 'init') { this.livePreviewConfig = live_preview; From 477ba30fff164e40d91c9fe69daf4cc0961b753e Mon Sep 17 00:00:00 2001 From: Faraaz Biyabani Date: Tue, 16 Jan 2024 10:58:11 +0530 Subject: [PATCH 3/8] feat: prefer cslp__meta but keep current reference extraction logic --- src/live-preview/index.js | 352 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 339 insertions(+), 13 deletions(-) diff --git a/src/live-preview/index.js b/src/live-preview/index.js index 1c7ab82..46179c6 100644 --- a/src/live-preview/index.js +++ b/src/live-preview/index.js @@ -4,6 +4,9 @@ import isEmpty from "lodash.isempty"; import cloneDeep from "lodash.clonedeep"; import { Storage } from "./storage-helper"; +// max depth for nested references +const MAX_DEPTH_ALLOWED = 5; + export class ContentstackGatsby { config; stackSdk; @@ -18,7 +21,7 @@ export class ContentstackGatsby { content_type_uid: "", entry_uid: "" } - + const stackConfig = { api_key: config.api_key, delivery_token: config.delivery_token, @@ -32,14 +35,261 @@ export class ContentstackGatsby { }, } this.stackSdk = contentstack.Stack(stackConfig); + // reference fields in various CTs and the CTs they refer + this.referenceFieldsStorage = new Storage( + window.sessionStorage, + 'reference_fields' + ); + this.referenceFields = this.referenceFieldsStorage.get(); this.statusStorage = new Storage(window.sessionStorage, "status") + + // json rte fields in various CTs + this.jsonRteFieldsStorage = new Storage( + window.sessionStorage, + 'json_rte_fields' + ); + this.jsonRteFields = this.jsonRteFieldsStorage.get(); + + // only field paths extracted from the above map for current CT + this.referenceFieldPaths = []; + + // store content types in LP site's session storage + this.contentTypesStorage = new Storage( + window.sessionStorage, + 'content_types' + ); + this.contentTypes = this.contentTypesStorage.get(); } setHost(host) { this.stackSdk.setHost(host); } + /** + * @deprecated With the `cslp__meta` query field, this should not be required + * @param {Object.} entry + */ + static addContentTypeUidFromTypename(entry) { + if (typeof entry === "undefined") { + throw new TypeError("entry cannot be empty"); + } + if (entry === null) { + throw new TypeError("entry cannot be null") + } + if (typeof entry !== "object") { + throw new TypeError("entry must be an object") + } + if (Array.isArray(entry)) { + throw new TypeError("entry cannot be an object, pass an instance of entry") + } + + traverse(entry) + + function traverse(field) { + if (!field || typeof field !== "object") { + return; + } + if (Array.isArray(field)) { + field.forEach((instance) => traverse(instance)) + } + if (Object.hasOwnProperty.call(field, "__typename") && typeof field.__typename == "string") { + field._content_type_uid = field.__typename.split("_").slice(1).join("_"); + } + Object.values(field).forEach((subField) => traverse(subField)) + } + } + + async fetchContentTypes(uids) { + try { + const result = await this.stackSdk.getContentTypes({ + query: { uid: { $in: uids } }, + include_global_field_schema: true, + }); + if (result) { + const contentTypes = {}; + result.content_types.forEach(ct => { + contentTypes[ct.uid] = ct; + }); + return contentTypes; + } + } catch (error) { + console.error('ContentstackGatsby - Failed to fetch content types'); + throw error; + } + } + + async getContentTypes(uids) { + // fetch and filter only content types that are not available in cache + const uidsToFetch = uids.filter(uid => !this.contentTypes[uid]); + if (!uidsToFetch.length) { + return this.contentTypes; + } + const types = await this.fetchContentTypes(uidsToFetch); + uidsToFetch.forEach(uid => { + // TODO need to set it in two places, can be better + this.contentTypes[uid] = types[uid]; + this.contentTypesStorage.set(uid, types[uid]); + }); + return this.contentTypes; + } + + async extractReferences( + refPathMap = {}, + status, + jsonRtePaths = [], + depth = MAX_DEPTH_ALLOWED, + seen = [] + ) { + if (depth <= 0) { + return refPathMap; + } + const uids = [...new Set(Object.values(refPathMap).flat())]; + const contentTypes = await this.getContentTypes(uids); + const refPathsCount = Object.keys(refPathMap).length; + const explorePaths = Object.entries(refPathMap).filter( + ([path]) => !seen.includes(path) + ); + for (const [refPath, refUids] of explorePaths) { + // mark this reference path as seen + seen.push(refPath); + for (const uid of refUids) { + let rPath = refPath.split('.'); + // when path is root, set path to [] + if (refPath === '') { + rPath = []; + } + + if (!status.hasLivePreviewEntryFound) { + status.hasLivePreviewEntryFound = this.isCurrentEntryEdited(uid) + } + + this.extractUids( + contentTypes[uid].schema, + rPath, + refPathMap, + jsonRtePaths + ); + } + } + if (Object.keys(refPathMap).length > refPathsCount) { + await this.extractReferences(refPathMap, status, jsonRtePaths, depth - 1, seen); + } + return { refPathMap, jsonRtePaths }; + } + + extractUids(schema, pathPrefix = [], refPathMap = {}, jsonRtePaths = []) { + const referredUids = []; + for (const field of schema) { + const fieldPath = [...pathPrefix, field.uid]; + if ( + field.data_type === 'reference' && + Array.isArray(field.reference_to) && + field.reference_to.length > 0 + ) { + referredUids.push(...field.reference_to); + refPathMap[fieldPath.join('.')] = field.reference_to; + } else if ( + field.data_type === 'blocks' && + field.blocks && + field.blocks.length > 0 + ) { + for (const block of field.blocks) { + const { referredUids: blockRefUids } = this.extractUids( + block.schema, + [...fieldPath, block.uid], + refPathMap, + jsonRtePaths + ); + referredUids.push(...blockRefUids); + } + } else if ( + field.data_type === 'group' && + field.schema && + field.schema.length > 0 + ) { + const { referredUids: groupRefUids } = this.extractUids( + field.schema, + [...fieldPath], + refPathMap, + jsonRtePaths + ); + referredUids.push(...groupRefUids); + } else if ( + field.data_type === 'json' && + field.field_metadata?.allow_json_rte + ) { + const rtePath = [...pathPrefix, field.uid].join('.'); + jsonRtePaths.push(rtePath); + } + } + return { referredUids, refPathMap }; + } + + /** + * Identify reference paths in user-provided data + * @param {any} data - entry data + * @param {string[]} currentPath - traversal path + * @param {string[]} referenceFieldPaths - content type reference paths + */ + identifyReferences(data, currentPath = [], referenceFieldPaths = []) { + const paths = []; + + for (const [k, v] of Object.entries(data)) { + if (!v) { + continue; + } + if (currentPath.length > 0) { + const refPath = currentPath.join('.'); + // if a reference path and not already collected, collect it + if (referenceFieldPaths.includes(refPath) && !paths.includes(refPath)) { + paths.push(refPath); + } + } + if (this.isNested(v)) { + const tempPath = [...currentPath]; + tempPath.push(k); + const p = this.identifyReferences(v, tempPath, referenceFieldPaths); + paths.push(...p); + } else if (Array.isArray(v)) { + const tempPath = [...currentPath]; + tempPath.push(k); + if (v.length > 0) { + // need to go over all refs since each of them could be of different + // content type and might contain refs + for (const val of v) { + const p = this.identifyReferences( + val, + tempPath, + referenceFieldPaths + ); + paths.push(...p); + } + } + // no multiple ref present, Gatsby value -> [] (empty array) + // no single ref present, Gatsby value -> [] + else if ( + v.length === 0 && + referenceFieldPaths.includes(tempPath.join('.')) + ) { + // it is a single or multiple ref + // also no idea what child references the user must be querying + // so need to get all child refs + paths.push(tempPath.join('.')); + const childRefPaths = referenceFieldPaths.filter(path => + path.startsWith(tempPath) + ); + paths.push(...childRefPaths); + } + } + } + return [...new Set(paths)]; + } + + isCurrentEntryEdited(entryContentType) { + return entryContentType === this.livePreviewConfig?.content_type_uid + } + async fetchEntry(entryUid, contentTypeUid, referencePaths = [], jsonRtePaths = []) { const entry = await this.stackSdk .ContentType(contentTypeUid) @@ -77,7 +327,76 @@ export class ContentstackGatsby { return values; } + async getUsingTypeName(data) { + const receivedData = cloneDeep(data); + const { live_preview } = this.stackSdk; + + let status = { + hasLivePreviewEntryFound: false + } + + this.livePreviewConfig = live_preview; + + const contentTypeUid = receivedData.__typename.split("_").slice(1).join("_") + const entryUid = receivedData.uid; + + status = this.statusStorage.get(contentTypeUid) ?? { hasLivePreviewEntryFound: this.isCurrentEntryEdited(contentTypeUid) } + + if ( + isEmpty(this.referenceFields[contentTypeUid]) || + isEmpty(this.jsonRteFields[contentTypeUid]) + ) { + try { + const { refPathMap, jsonRtePaths } = await this.extractReferences({ + '': [contentTypeUid], + }, status); + // store reference paths + this.referenceFields[contentTypeUid] = refPathMap; + this.referenceFieldsStorage.set( + contentTypeUid, + this.referenceFields[contentTypeUid] + ); + // store json rte paths + this.jsonRteFields[contentTypeUid] = jsonRtePaths; + this.jsonRteFieldsStorage.set( + contentTypeUid, + this.jsonRteFields[contentTypeUid] + ); + } + catch (error) { + console.error("Contentstack Gatsby (Live Preview): an error occurred while determining reference paths", error); + console.log("Contentstack Gatsby (Live Preview): unable to determine reference paths. This may have occurred due to the way the content types refer each other. Please try including the cslp__meta field in your query."); + return receivedData; + } + } + + let referencePaths = Object.keys(this.referenceFields[contentTypeUid]); + referencePaths = referencePaths.filter(field => !!field) + + const paths = this.identifyReferences( + receivedData, + [], + referencePaths + ); + + this.statusStorage.set(contentTypeUid, status) + + if (!status.hasLivePreviewEntryFound) { + return receivedData + } + + const entry = await this.fetchEntry( + entryUid, + contentTypeUid, + paths, + this.jsonRteFields[contentTypeUid] ?? [] + ); + return entry; + } + async get(data) { + // if cslp__meta is found, use the paths from cslp__meta + // else use the old method to determine the paths if (data === null) { console.warn("Contentstack Gatsby (Live Preview): null was passed to get()"); return data; @@ -89,6 +408,9 @@ export class ContentstackGatsby { const dataCloned = cloneDeep(data); delete dataCloned["$"]; + // old method metadata + const hasTypeNameAndUid = dataCloned.uid && dataCloned.__typename; + // new method metadata const hasCslpMetaAtRoot = dataCloned.cslp__meta; const multipleEntriesKey = Object.keys(dataCloned); const hasSingleEntry = !hasCslpMetaAtRoot && multipleEntriesKey.length === 1; @@ -109,7 +431,7 @@ export class ContentstackGatsby { receivedData.length > 1 && receivedData.every((item) => item === null || item.cslp__meta); - if (!hasCslpMeta && !hasMultipleCslpMeta) { + if (!hasCslpMeta && !hasMultipleCslpMeta && !hasTypeNameAndUid) { throw new Error("Contentstack Gatsby (Live Preview): Entry data must contain cslp__meta for live preview") } @@ -137,20 +459,24 @@ export class ContentstackGatsby { return dataCloned; } } + else if (hasCslpMeta) { + const entryCslpMeta = receivedData.cslp__meta; + const contentTypeUid = entryCslpMeta.contentTypeUid; + const entryUid = entryCslpMeta.entryUid; - const entryCslpMeta = receivedData.cslp__meta; - const contentTypeUid = entryCslpMeta.contentTypeUid; - const entryUid = entryCslpMeta.entryUid; + if (!entryUid || !contentTypeUid) { + console.warn("Contentstack Gatsby (Live Preview): no entry uid or content type uid was found inside cslp__meta") + return dataCloned; + } - if (!entryUid || !contentTypeUid) { - console.warn("Contentstack Gatsby (Live Preview): no entry uid or content type uid was found inside cslp__meta") - return dataCloned; + const refPaths = entryCslpMeta.referencePaths ?? []; + const rtePaths = entryCslpMeta.jsonRtePaths ?? []; + const entry = await this.fetchEntry(entryUid, contentTypeUid, refPaths, rtePaths); + return entry; + } + else if (hasTypeNameAndUid) { + return await this.getUsingTypeName(dataCloned); } - - const refPaths = entryCslpMeta.referencePaths ?? []; - const rtePaths = entryCslpMeta.jsonRtePaths ?? []; - const entry = await this.fetchEntry(entryUid, contentTypeUid, refPaths, rtePaths); - return entry; } return dataCloned; } From b7e9abb636fa838386d1e981b290027d9e2a922b Mon Sep 17 00:00:00 2001 From: Faraaz Biyabani Date: Tue, 16 Jan 2024 11:03:42 +0530 Subject: [PATCH 4/8] chore: update year to 2024 in license --- LICENCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE b/LICENCE index a48530c..f99e9ee 100644 --- a/LICENCE +++ b/LICENCE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Contentstack +Copyright (c) 2024 Contentstack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 0fcadaab61f67e5190170d333db7a474181930e7 Mon Sep 17 00:00:00 2001 From: Faraaz Biyabani Date: Tue, 16 Jan 2024 11:06:14 +0530 Subject: [PATCH 5/8] chore: update error message --- src/live-preview/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/live-preview/index.js b/src/live-preview/index.js index 46179c6..8b80566 100644 --- a/src/live-preview/index.js +++ b/src/live-preview/index.js @@ -114,7 +114,7 @@ export class ContentstackGatsby { return contentTypes; } } catch (error) { - console.error('ContentstackGatsby - Failed to fetch content types'); + console.error("Contentstack Gatsby (Live Preview): failed to fetch content types"); throw error; } } From f644c2a7747a600de8d361381f7651a9fc68b47c Mon Sep 17 00:00:00 2001 From: Faraaz Biyabani Date: Tue, 16 Jan 2024 11:51:00 +0530 Subject: [PATCH 6/8] feat: check if live preview is enabled --- src/live-preview/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/live-preview/index.js b/src/live-preview/index.js index 8b80566..95baf7a 100644 --- a/src/live-preview/index.js +++ b/src/live-preview/index.js @@ -397,6 +397,10 @@ export class ContentstackGatsby { async get(data) { // if cslp__meta is found, use the paths from cslp__meta // else use the old method to determine the paths + if (this.stackSdk.live_preview && !this.stackSdk.live_preview?.enable) { + console.warn("Contentstack Gatsby (Live Preview): live preview is disabled in config"); + return data; + } if (data === null) { console.warn("Contentstack Gatsby (Live Preview): null was passed to get()"); return data; From fb4b36424520e2a2a3b9a03a4c76567cf23e3dfe Mon Sep 17 00:00:00 2001 From: Faraaz Biyabani Date: Wed, 17 Jan 2024 10:35:34 +0530 Subject: [PATCH 7/8] chore: build --- create-resolvers.js | 41 +- entry-data.js | 9 +- fetch.js | 28 +- hooks/index.js | 13 + hooks/use-contentstack-image.js | 42 ++ live-preview/getCslpMetaPaths.js | 129 ++++++ live-preview/index.js | 687 +++++++++++++++++++++++++++++++ live-preview/resolveCslpMeta.js | 103 +++++ live-preview/storage-helper.js | 59 +++ node-helper.js | 9 +- plugin-options-schema.js | 4 +- tests/getHeaders.test.js | 31 ++ utils.js | 10 + 13 files changed, 1146 insertions(+), 19 deletions(-) create mode 100644 hooks/index.js create mode 100644 hooks/use-contentstack-image.js create mode 100644 live-preview/getCslpMetaPaths.js create mode 100644 live-preview/index.js create mode 100644 live-preview/resolveCslpMeta.js create mode 100644 live-preview/storage-helper.js create mode 100644 tests/getHeaders.test.js diff --git a/create-resolvers.js b/create-resolvers.js index 6ac728b..835abd4 100644 --- a/create-resolvers.js +++ b/create-resolvers.js @@ -13,13 +13,15 @@ var _require = require('./utils'), var _require2 = require('./normalize'), makeEntryNodeUid = _require2.makeEntryNodeUid, makeAssetNodeUid = _require2.makeAssetNodeUid; +var _require3 = require('./live-preview/resolveCslpMeta'), + resolveCslpMeta = _require3.resolveCslpMeta; exports.createResolvers = /*#__PURE__*/function () { var _ref = (0, _asyncToGenerator2["default"])(function (_ref2, configOptions) { var createResolvers = _ref2.createResolvers, cache = _ref2.cache, createNodeId = _ref2.createNodeId; return /*#__PURE__*/_regenerator["default"].mark(function _callee() { - var resolvers, typePrefix, _yield$Promise$all, _yield$Promise$all2, fileFields, references, groups, jsonRteFields; + var resolvers, typePrefix, _yield$Promise$all, _yield$Promise$all2, fileFields, references, groups, jsonRteFields, contentTypes, contentTypeMap; return _regenerator["default"].wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: @@ -34,6 +36,41 @@ exports.createResolvers = /*#__PURE__*/function () { references = _yield$Promise$all2[1]; groups = _yield$Promise$all2[2]; jsonRteFields = _yield$Promise$all2[3]; + _context.next = 12; + return cache.get(typePrefix); + case 12: + contentTypes = _context.sent; + contentTypeMap = {}; + contentTypes.forEach(function (item) { + contentTypeMap[item.uid] = item; + }); + contentTypes.forEach(function (contentType) { + resolvers["".concat(typePrefix, "_").concat(contentType.uid)] = { + "cslp__meta": { + type: "JSON", + resolve: function resolve(source, args, context, info) { + try { + return resolveCslpMeta({ + source: source, + args: args, + context: context, + info: info, + contentTypeMap: contentTypeMap, + typePrefix: typePrefix + }); + } catch (error) { + var _error$message; + console.error("ContentstackGatsby (Live Preview):", error); + return { + error: { + message: (_error$message = error.message) !== null && _error$message !== void 0 ? _error$message : "failed to resolve cslp__meta" + } + }; + } + } + } + }; + }); fileFields && fileFields.forEach(function (fileField) { resolvers[fileField.parent] = _objectSpread(_objectSpread({}, resolvers[fileField.parent]), (0, _defineProperty2["default"])({}, fileField.field.uid, { resolve: function resolve(source, args, context) { @@ -115,7 +152,7 @@ exports.createResolvers = /*#__PURE__*/function () { })); }); createResolvers(resolvers); - case 15: + case 21: case "end": return _context.stop(); } diff --git a/entry-data.js b/entry-data.js index e9ecb6c..13a1c6b 100644 --- a/entry-data.js +++ b/entry-data.js @@ -69,12 +69,14 @@ var FetchDefaultEntries = /*#__PURE__*/function (_FetchEntries) { syncEntryParams = syncEntryToken ? { sync_token: syncEntryToken } : { - init: true + init: true, + limit: configOptions.limit > 100 ? 50 : configOptions.limit }; syncAssetParams = syncAssetToken ? { sync_token: syncAssetToken } : { - init: true + init: true, + limit: configOptions.limit > 100 ? 50 : configOptions.limit }; syncEntryParams.type = 'entry_published,entry_unpublished,entry_deleted'; syncAssetParams.type = 'asset_published,asset_unpublished,asset_deleted'; @@ -101,7 +103,8 @@ var FetchDefaultEntries = /*#__PURE__*/function (_FetchEntries) { syncParams = syncToken ? { sync_token: syncToken } : { - init: true + init: true, + limit: configOptions.limit > 100 ? 50 : configOptions.limit }; _context2.next = 35; return fn.apply(null, [syncParams, configOptions]); diff --git a/fetch.js b/fetch.js index 4f1f027..23d620d 100644 --- a/fetch.js +++ b/fetch.js @@ -11,10 +11,13 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } var preferDefault = function preferDefault(m) { return m && m["default"] || m; }; @@ -36,7 +39,8 @@ var _require3 = require('./entry-data'), FetchSpecifiedLocalesEntries = _require3.FetchSpecifiedLocalesEntries, FetchSpecifiedLocalesAndContentTypesEntries = _require3.FetchSpecifiedLocalesAndContentTypesEntries; var _require4 = require('./utils'), - CODES = _require4.CODES; + CODES = _require4.CODES, + getCustomHeaders = _require4.getCustomHeaders; var OPTION_CLASS_MAPPING = { '': FetchDefaultContentTypes, contentTypes: FetchSpecifiedContentTypes, @@ -249,12 +253,12 @@ var fetchCsData = /*#__PURE__*/function () { queryParams = queryString.stringify(query); apiUrl = "".concat(config.cdn, "/").concat(url, "?").concat(queryParams); option = { - headers: { + headers: _objectSpread({ 'X-User-Agent': "contentstack-gatsby-source-plugin-".concat(version), api_key: config === null || config === void 0 ? void 0 : config.api_key, access_token: config === null || config === void 0 ? void 0 : config.delivery_token, branch: config !== null && config !== void 0 && config.branch ? config.branch : 'main' - } + }, getCustomHeaders(config === null || config === void 0 ? void 0 : config.enableEarlyAccessKey, config === null || config === void 0 ? void 0 : config.enableEarlyAccessValue)) }; _context6.next = 8; return getData(apiUrl, option); @@ -284,11 +288,14 @@ var getPagedData = /*#__PURE__*/function () { case 0: query.skip = skip; //if limit is greater than 100, it will throw ann error that limit cannot exceed 100. - query.limit = limit > 100 ? (console.error('Limit cannot exceed 100.'), 100) : limit; + if (limit > 100) { + console.error('Limit cannot exceed 100. Setting limit to 50.'); + } + query.limit = limit > 100 ? 50 : limit; query.include_global_field_schema = true; - _context7.next = 5; + _context7.next = 6; return fetchCsData(url, config, query); - case 5: + case 6: response = _context7.sent; if (!aggregatedResponse) { aggregatedResponse = response[responseKey]; @@ -296,13 +303,13 @@ var getPagedData = /*#__PURE__*/function () { aggregatedResponse = aggregatedResponse.concat(response[responseKey]); } if (!(skip + limit <= response.count)) { - _context7.next = 9; + _context7.next = 10; break; } return _context7.abrupt("return", getPagedData(url, config, responseKey, query = {}, skip + limit, limit, aggregatedResponse)); - case 9: - return _context7.abrupt("return", aggregatedResponse); case 10: + return _context7.abrupt("return", aggregatedResponse); + case 11: case "end": return _context7.stop(); } @@ -393,8 +400,9 @@ var getSyncData = /*#__PURE__*/function () { _iterator.f(); return _context8.finish(26); case 29: + syncToken = []; return _context8.abrupt("return", aggregatedResponse); - case 30: + case 31: case "end": return _context8.stop(); } diff --git a/hooks/index.js b/hooks/index.js new file mode 100644 index 0000000..e2b3197 --- /dev/null +++ b/hooks/index.js @@ -0,0 +1,13 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useContentstackImage", { + enumerable: true, + get: function get() { + return _useContentstackImage.useContentstackImage; + } +}); +var _useContentstackImage = require("./use-contentstack-image"); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/hooks/use-contentstack-image.js b/hooks/use-contentstack-image.js new file mode 100644 index 0000000..27ae205 --- /dev/null +++ b/hooks/use-contentstack-image.js @@ -0,0 +1,42 @@ +"use strict"; + +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.useContentstackImage = useContentstackImage; +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); +var _gatsbyPluginImage = require("gatsby-plugin-image"); +var _react = require("react"); +var _imageHelper = require("../image-helper"); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function useContentstackImage(image, options) { + return (0, _react.useMemo)(function () { + return (0, _gatsbyPluginImage.getImageData)({ + baseUrl: image.url, + height: options === null || options === void 0 ? void 0 : options.height, + width: options === null || options === void 0 ? void 0 : options.width, + formats: options === null || options === void 0 ? void 0 : options.formats, + sourceWidth: image === null || image === void 0 ? void 0 : image.width, + sourceHeight: image === null || image === void 0 ? void 0 : image.height, + layout: options === null || options === void 0 ? void 0 : options.layout, + urlBuilder: function urlBuilder(_ref) { + var baseUrl = _ref.baseUrl, + options = _ref.options, + height = _ref.height, + width = _ref.width, + format = _ref.format; + return (0, _imageHelper.createUrl)(baseUrl, _objectSpread(_objectSpread({}, options), {}, { + height: height, + width: width, + toFormat: format + })); + }, + pluginName: 'gatsby-source-contentstack', + // these options are internally passed to urlBuilder + options: options + }); + }, [image, options]); +} +//# sourceMappingURL=use-contentstack-image.js.map \ No newline at end of file diff --git a/live-preview/getCslpMetaPaths.js b/live-preview/getCslpMetaPaths.js new file mode 100644 index 0000000..8103aa1 --- /dev/null +++ b/live-preview/getCslpMetaPaths.js @@ -0,0 +1,129 @@ +"use strict"; + +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.getCslpMetaPaths = getCslpMetaPaths; +var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); +var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } +function getCslpMetaPaths(selectionSet) { + var _schema$field_metadat; + var path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; + var schema = arguments.length > 2 ? arguments[2] : undefined; + var fragments = arguments.length > 3 ? arguments[3] : undefined; + var contentTypeMap = arguments.length > 4 ? arguments[4] : undefined; + var typePrefix = arguments.length > 5 ? arguments[5] : undefined; + var paths = { + referencePaths: [], + jsonRtePaths: [] + }; + var refPaths = paths.referencePaths; + var rtePaths = paths.jsonRtePaths; + if (schema.data_type === "reference" && path) { + refPaths.push(path); + } + if (schema.data_type === "json" && (_schema$field_metadat = schema.field_metadata) !== null && _schema$field_metadat !== void 0 && _schema$field_metadat.allow_json_rte) { + rtePaths.push(path); + } + if (!selectionSet || !selectionSet.selections) { + return paths; + } + var _iterator = _createForOfIteratorHelper(selectionSet.selections), + _step; + try { + var _loop = function _loop() { + var _selection$name; + var selection = _step.value; + // exit when selection.kind is not Field, SelectionSet or InlineFragment + // selection.name is not present for selection.kind is "InlineFragment" + if ((selection === null || selection === void 0 ? void 0 : (_selection$name = selection.name) === null || _selection$name === void 0 ? void 0 : _selection$name.value) === "cslp__meta") { + return "continue"; + } + if (selection.selectionSet || selection.kind === "Field" || selection.kind === "InlineFragment" || selection.kind === "FragmentSpread") { + var _selection$name2, _selection$typeCondit, _selection$typeCondit2; + var fragmentName = (_selection$name2 = selection.name) === null || _selection$name2 === void 0 ? void 0 : _selection$name2.value; + var fragmentDefinition = fragments[fragmentName]; + var inlineFragmentNodeType = (_selection$typeCondit = selection.typeCondition) === null || _selection$typeCondit === void 0 ? void 0 : (_selection$typeCondit2 = _selection$typeCondit.name) === null || _selection$typeCondit2 === void 0 ? void 0 : _selection$typeCondit2.value; + + // Fragment + // note - when a fragment is used inside a reference field, the reference field + // path gets added twice, this can maybe avoided by re-structuring the code, + // but a Set works fine + if (selection.kind === "FragmentSpread" && fragmentDefinition) { + var fragmentSpreadPaths = getCslpMetaPaths(fragmentDefinition.selectionSet, path, schema, fragments, contentTypeMap, typePrefix); + combineCslpMetaPaths(fragmentSpreadPaths, paths); + } + // InlineFragment (ref_multiple) + else if (selection.kind === "InlineFragment" && inlineFragmentNodeType) { + var contentTypeUid = inlineFragmentNodeType.replace("".concat(typePrefix, "_"), ""); + if (!contentTypeUid || !(contentTypeUid in contentTypeMap)) { + return { + v: paths + }; + } + var contentTypeSchema = contentTypeMap[contentTypeUid]; + var inlineFragmentPaths = getCslpMetaPaths(selection.selectionSet, path, contentTypeSchema, fragments, contentTypeMap, typePrefix); + combineCslpMetaPaths(inlineFragmentPaths, paths); + } + // SelectionSet (all fields that can have nested properties) + else { + var _schema$blocks; + var nestedFields = (_schema$blocks = schema === null || schema === void 0 ? void 0 : schema.blocks) !== null && _schema$blocks !== void 0 ? _schema$blocks : schema.schema; + // cannot traverse inside file or link schema + if (schema.data_type === "file" || schema.data_type === "link") { + return { + v: paths + }; + } + // when a reference, change nested fields to schema of referenced CT + if (schema.data_type === "reference" && schema.reference_to) { + nestedFields = contentTypeMap[schema.reference_to[0]].schema; + } + var nestedFieldSchema = nestedFields.find(function (item) { + return item.uid === selection.name.value; + }); + if (nestedFieldSchema) { + var nextPath = []; + if (path) { + nextPath = path.split("."); + } + nextPath.push(selection.name.value); + var metaPaths = getCslpMetaPaths(selection.selectionSet, nextPath.join("."), nestedFieldSchema, fragments, contentTypeMap, typePrefix); + combineCslpMetaPaths(metaPaths, paths); + } + } + } + }; + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _ret = _loop(); + if (_ret === "continue") continue; + if ((0, _typeof2["default"])(_ret) === "object") return _ret.v; + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + return { + referencePaths: Array.from(new Set(paths.referencePaths)), + jsonRtePaths: Array.from(new Set(paths.jsonRtePaths)) + }; +} + +/** + * @typedef {{referencePaths: string[], jsonRtePaths: string[]}} paths + * @param {paths} source + * @param {paths} target + * @returns merges the path fields from the source into the target's path fields + */ +function combineCslpMetaPaths(source, target) { + var _target$referencePath, _target$jsonRtePaths; + (_target$referencePath = target.referencePaths).push.apply(_target$referencePath, (0, _toConsumableArray2["default"])(source.referencePaths)); + (_target$jsonRtePaths = target.jsonRtePaths).push.apply(_target$jsonRtePaths, (0, _toConsumableArray2["default"])(source.jsonRtePaths)); + return target; +} +//# sourceMappingURL=getCslpMetaPaths.js.map \ No newline at end of file diff --git a/live-preview/index.js b/live-preview/index.js new file mode 100644 index 0000000..aebd446 --- /dev/null +++ b/live-preview/index.js @@ -0,0 +1,687 @@ +"use strict"; + +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ContentstackGatsby = void 0; +var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); +var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); +var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); +var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); +var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); +var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); +var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); +var _contentstack = _interopRequireDefault(require("contentstack")); +var _utils = require("@contentstack/utils"); +var _lodash = _interopRequireDefault(require("lodash.isempty")); +var _lodash2 = _interopRequireDefault(require("lodash.clonedeep")); +var _storageHelper = require("./storage-helper"); +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +// max depth for nested references +var MAX_DEPTH_ALLOWED = 5; +var ContentstackGatsby = /*#__PURE__*/function () { + function ContentstackGatsby(config) { + (0, _classCallCheck2["default"])(this, ContentstackGatsby); + (0, _defineProperty2["default"])(this, "config", void 0); + (0, _defineProperty2["default"])(this, "stackSdk", void 0); + (0, _defineProperty2["default"])(this, "contentTypes", void 0); + (0, _defineProperty2["default"])(this, "referenceFields", void 0); + (0, _defineProperty2["default"])(this, "referenceFieldPaths", void 0); + this.config = config; + this.livePreviewConfig = { + hash: "", + content_type_uid: "", + entry_uid: "" + }; + var stackConfig = _objectSpread(_objectSpread(_objectSpread({ + api_key: config.api_key, + delivery_token: config.delivery_token, + environment: config.environment + }, config.region && { + region: config.region + }), config.branch && { + branch: config.branch + }), {}, { + live_preview: { + host: config.live_preview.host, + management_token: config.live_preview.management_token, + enable: config.live_preview.enable + } + }); + this.stackSdk = _contentstack["default"].Stack(stackConfig); + // reference fields in various CTs and the CTs they refer + this.referenceFieldsStorage = new _storageHelper.Storage(window.sessionStorage, 'reference_fields'); + this.referenceFields = this.referenceFieldsStorage.get(); + this.statusStorage = new _storageHelper.Storage(window.sessionStorage, "status"); + + // json rte fields in various CTs + this.jsonRteFieldsStorage = new _storageHelper.Storage(window.sessionStorage, 'json_rte_fields'); + this.jsonRteFields = this.jsonRteFieldsStorage.get(); + + // only field paths extracted from the above map for current CT + this.referenceFieldPaths = []; + + // store content types in LP site's session storage + this.contentTypesStorage = new _storageHelper.Storage(window.sessionStorage, 'content_types'); + this.contentTypes = this.contentTypesStorage.get(); + } + (0, _createClass2["default"])(ContentstackGatsby, [{ + key: "setHost", + value: function setHost(host) { + this.stackSdk.setHost(host); + } + + /** + * @deprecated With the `cslp__meta` query field, this should not be required + * @param {Object.} entry + */ + }, { + key: "fetchContentTypes", + value: function () { + var _fetchContentTypes = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(uids) { + var result, contentTypes; + return _regenerator["default"].wrap(function _callee$(_context) { + while (1) switch (_context.prev = _context.next) { + case 0: + _context.prev = 0; + _context.next = 3; + return this.stackSdk.getContentTypes({ + query: { + uid: { + $in: uids + } + }, + include_global_field_schema: true + }); + case 3: + result = _context.sent; + if (!result) { + _context.next = 8; + break; + } + contentTypes = {}; + result.content_types.forEach(function (ct) { + contentTypes[ct.uid] = ct; + }); + return _context.abrupt("return", contentTypes); + case 8: + _context.next = 14; + break; + case 10: + _context.prev = 10; + _context.t0 = _context["catch"](0); + console.error("Contentstack Gatsby (Live Preview): failed to fetch content types"); + throw _context.t0; + case 14: + case "end": + return _context.stop(); + } + }, _callee, this, [[0, 10]]); + })); + function fetchContentTypes(_x) { + return _fetchContentTypes.apply(this, arguments); + } + return fetchContentTypes; + }() + }, { + key: "getContentTypes", + value: function () { + var _getContentTypes = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(uids) { + var _this = this; + var uidsToFetch, types; + return _regenerator["default"].wrap(function _callee2$(_context2) { + while (1) switch (_context2.prev = _context2.next) { + case 0: + // fetch and filter only content types that are not available in cache + uidsToFetch = uids.filter(function (uid) { + return !_this.contentTypes[uid]; + }); + if (uidsToFetch.length) { + _context2.next = 3; + break; + } + return _context2.abrupt("return", this.contentTypes); + case 3: + _context2.next = 5; + return this.fetchContentTypes(uidsToFetch); + case 5: + types = _context2.sent; + uidsToFetch.forEach(function (uid) { + // TODO need to set it in two places, can be better + _this.contentTypes[uid] = types[uid]; + _this.contentTypesStorage.set(uid, types[uid]); + }); + return _context2.abrupt("return", this.contentTypes); + case 8: + case "end": + return _context2.stop(); + } + }, _callee2, this); + })); + function getContentTypes(_x2) { + return _getContentTypes.apply(this, arguments); + } + return getContentTypes; + }() + }, { + key: "extractReferences", + value: function () { + var _extractReferences = (0, _asyncToGenerator2["default"])(function () { + var _this2 = this; + var refPathMap = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var status = arguments.length > 1 ? arguments[1] : undefined; + var jsonRtePaths = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; + var depth = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : MAX_DEPTH_ALLOWED; + var seen = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : []; + return /*#__PURE__*/_regenerator["default"].mark(function _callee3() { + var uids, contentTypes, refPathsCount, explorePaths, _iterator, _step, _step$value, refPath, refUids, _iterator2, _step2, uid, rPath; + return _regenerator["default"].wrap(function _callee3$(_context3) { + while (1) switch (_context3.prev = _context3.next) { + case 0: + if (!(depth <= 0)) { + _context3.next = 2; + break; + } + return _context3.abrupt("return", refPathMap); + case 2: + uids = (0, _toConsumableArray2["default"])(new Set(Object.values(refPathMap).flat())); + _context3.next = 5; + return _this2.getContentTypes(uids); + case 5: + contentTypes = _context3.sent; + refPathsCount = Object.keys(refPathMap).length; + explorePaths = Object.entries(refPathMap).filter(function (_ref) { + var _ref2 = (0, _slicedToArray2["default"])(_ref, 1), + path = _ref2[0]; + return !seen.includes(path); + }); + _iterator = _createForOfIteratorHelper(explorePaths); + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + _step$value = (0, _slicedToArray2["default"])(_step.value, 2), refPath = _step$value[0], refUids = _step$value[1]; + // mark this reference path as seen + seen.push(refPath); + _iterator2 = _createForOfIteratorHelper(refUids); + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + uid = _step2.value; + rPath = refPath.split('.'); // when path is root, set path to [] + if (refPath === '') { + rPath = []; + } + if (!status.hasLivePreviewEntryFound) { + status.hasLivePreviewEntryFound = _this2.isCurrentEntryEdited(uid); + } + _this2.extractUids(contentTypes[uid].schema, rPath, refPathMap, jsonRtePaths); + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + if (!(Object.keys(refPathMap).length > refPathsCount)) { + _context3.next = 13; + break; + } + _context3.next = 13; + return _this2.extractReferences(refPathMap, status, jsonRtePaths, depth - 1, seen); + case 13: + return _context3.abrupt("return", { + refPathMap: refPathMap, + jsonRtePaths: jsonRtePaths + }); + case 14: + case "end": + return _context3.stop(); + } + }, _callee3); + })(); + }); + function extractReferences() { + return _extractReferences.apply(this, arguments); + } + return extractReferences; + }() + }, { + key: "extractUids", + value: function extractUids(schema) { + var pathPrefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; + var refPathMap = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var jsonRtePaths = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : []; + var referredUids = []; + var _iterator3 = _createForOfIteratorHelper(schema), + _step3; + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var _field$field_metadata; + var field = _step3.value; + var fieldPath = [].concat((0, _toConsumableArray2["default"])(pathPrefix), [field.uid]); + if (field.data_type === 'reference' && Array.isArray(field.reference_to) && field.reference_to.length > 0) { + referredUids.push.apply(referredUids, (0, _toConsumableArray2["default"])(field.reference_to)); + refPathMap[fieldPath.join('.')] = field.reference_to; + } else if (field.data_type === 'blocks' && field.blocks && field.blocks.length > 0) { + var _iterator4 = _createForOfIteratorHelper(field.blocks), + _step4; + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var block = _step4.value; + var _this$extractUids = this.extractUids(block.schema, [].concat((0, _toConsumableArray2["default"])(fieldPath), [block.uid]), refPathMap, jsonRtePaths), + blockRefUids = _this$extractUids.referredUids; + referredUids.push.apply(referredUids, (0, _toConsumableArray2["default"])(blockRefUids)); + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + } else if (field.data_type === 'group' && field.schema && field.schema.length > 0) { + var _this$extractUids2 = this.extractUids(field.schema, (0, _toConsumableArray2["default"])(fieldPath), refPathMap, jsonRtePaths), + groupRefUids = _this$extractUids2.referredUids; + referredUids.push.apply(referredUids, (0, _toConsumableArray2["default"])(groupRefUids)); + } else if (field.data_type === 'json' && (_field$field_metadata = field.field_metadata) !== null && _field$field_metadata !== void 0 && _field$field_metadata.allow_json_rte) { + var rtePath = [].concat((0, _toConsumableArray2["default"])(pathPrefix), [field.uid]).join('.'); + jsonRtePaths.push(rtePath); + } + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + return { + referredUids: referredUids, + refPathMap: refPathMap + }; + } + + /** + * Identify reference paths in user-provided data + * @param {any} data - entry data + * @param {string[]} currentPath - traversal path + * @param {string[]} referenceFieldPaths - content type reference paths + */ + }, { + key: "identifyReferences", + value: function identifyReferences(data) { + var _this3 = this; + var currentPath = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; + var referenceFieldPaths = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; + var paths = []; + var _loop = function _loop() { + var _Object$entries$_i = (0, _slicedToArray2["default"])(_Object$entries[_i], 2), + k = _Object$entries$_i[0], + v = _Object$entries$_i[1]; + if (!v) { + return "continue"; + } + if (currentPath.length > 0) { + var refPath = currentPath.join('.'); + // if a reference path and not already collected, collect it + if (referenceFieldPaths.includes(refPath) && !paths.includes(refPath)) { + paths.push(refPath); + } + } + if (_this3.isNested(v)) { + var tempPath = (0, _toConsumableArray2["default"])(currentPath); + tempPath.push(k); + var p = _this3.identifyReferences(v, tempPath, referenceFieldPaths); + paths.push.apply(paths, (0, _toConsumableArray2["default"])(p)); + } else if (Array.isArray(v)) { + var _tempPath = (0, _toConsumableArray2["default"])(currentPath); + _tempPath.push(k); + if (v.length > 0) { + // need to go over all refs since each of them could be of different + // content type and might contain refs + var _iterator5 = _createForOfIteratorHelper(v), + _step5; + try { + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + var val = _step5.value; + var _p = _this3.identifyReferences(val, _tempPath, referenceFieldPaths); + paths.push.apply(paths, (0, _toConsumableArray2["default"])(_p)); + } + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + } + // no multiple ref present, Gatsby value -> [] (empty array) + // no single ref present, Gatsby value -> [] + else if (v.length === 0 && referenceFieldPaths.includes(_tempPath.join('.'))) { + // it is a single or multiple ref + // also no idea what child references the user must be querying + // so need to get all child refs + paths.push(_tempPath.join('.')); + var childRefPaths = referenceFieldPaths.filter(function (path) { + return path.startsWith(_tempPath); + }); + paths.push.apply(paths, (0, _toConsumableArray2["default"])(childRefPaths)); + } + } + }; + for (var _i = 0, _Object$entries = Object.entries(data); _i < _Object$entries.length; _i++) { + var _ret = _loop(); + if (_ret === "continue") continue; + } + return (0, _toConsumableArray2["default"])(new Set(paths)); + } + }, { + key: "isCurrentEntryEdited", + value: function isCurrentEntryEdited(entryContentType) { + var _this$livePreviewConf; + return entryContentType === ((_this$livePreviewConf = this.livePreviewConfig) === null || _this$livePreviewConf === void 0 ? void 0 : _this$livePreviewConf.content_type_uid); + } + }, { + key: "fetchEntry", + value: function () { + var _fetchEntry = (0, _asyncToGenerator2["default"])(function (entryUid, contentTypeUid) { + var _this4 = this; + var referencePaths = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; + var jsonRtePaths = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : []; + return /*#__PURE__*/_regenerator["default"].mark(function _callee4() { + var entry; + return _regenerator["default"].wrap(function _callee4$(_context4) { + while (1) switch (_context4.prev = _context4.next) { + case 0: + _context4.next = 2; + return _this4.stackSdk.ContentType(contentTypeUid).Entry(entryUid).includeReference(referencePaths).toJSON().fetch(); + case 2: + entry = _context4.sent; + if ((0, _lodash["default"])(entry)) { + _context4.next = 6; + break; + } + if (_this4.config.jsonRteToHtml) { + (0, _utils.jsonToHTML)({ + entry: entry, + paths: jsonRtePaths + }); + } + return _context4.abrupt("return", entry); + case 6: + case "end": + return _context4.stop(); + } + }, _callee4); + })(); + }); + function fetchEntry(_x3, _x4) { + return _fetchEntry.apply(this, arguments); + } + return fetchEntry; + }() + }, { + key: "isNested", + value: function isNested(value) { + if ((0, _typeof2["default"])(value) === 'object' && !Array.isArray(value) && value !== null) { + return true; + } + return false; + } + }, { + key: "unwrapEntryData", + value: function unwrapEntryData(data) { + var values = Object.values(data); + if (!values) { + return data; + } + if (values && values.length === 1) { + return values[0]; + } + return values; + } + }, { + key: "getUsingTypeName", + value: function () { + var _getUsingTypeName = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee5(data) { + var _this$statusStorage$g, _this$jsonRteFields$c; + var receivedData, live_preview, status, contentTypeUid, entryUid, _yield$this$extractRe, refPathMap, jsonRtePaths, referencePaths, paths, entry; + return _regenerator["default"].wrap(function _callee5$(_context5) { + while (1) switch (_context5.prev = _context5.next) { + case 0: + receivedData = (0, _lodash2["default"])(data); + live_preview = this.stackSdk.live_preview; + status = { + hasLivePreviewEntryFound: false + }; + this.livePreviewConfig = live_preview; + contentTypeUid = receivedData.__typename.split("_").slice(1).join("_"); + entryUid = receivedData.uid; + status = (_this$statusStorage$g = this.statusStorage.get(contentTypeUid)) !== null && _this$statusStorage$g !== void 0 ? _this$statusStorage$g : { + hasLivePreviewEntryFound: this.isCurrentEntryEdited(contentTypeUid) + }; + if (!((0, _lodash["default"])(this.referenceFields[contentTypeUid]) || (0, _lodash["default"])(this.jsonRteFields[contentTypeUid]))) { + _context5.next = 25; + break; + } + _context5.prev = 8; + _context5.next = 11; + return this.extractReferences({ + '': [contentTypeUid] + }, status); + case 11: + _yield$this$extractRe = _context5.sent; + refPathMap = _yield$this$extractRe.refPathMap; + jsonRtePaths = _yield$this$extractRe.jsonRtePaths; + // store reference paths + this.referenceFields[contentTypeUid] = refPathMap; + this.referenceFieldsStorage.set(contentTypeUid, this.referenceFields[contentTypeUid]); + // store json rte paths + this.jsonRteFields[contentTypeUid] = jsonRtePaths; + this.jsonRteFieldsStorage.set(contentTypeUid, this.jsonRteFields[contentTypeUid]); + _context5.next = 25; + break; + case 20: + _context5.prev = 20; + _context5.t0 = _context5["catch"](8); + console.error("Contentstack Gatsby (Live Preview): an error occurred while determining reference paths", _context5.t0); + console.log("Contentstack Gatsby (Live Preview): unable to determine reference paths. This may have occurred due to the way the content types refer each other. Please try including the cslp__meta field in your query."); + return _context5.abrupt("return", receivedData); + case 25: + referencePaths = Object.keys(this.referenceFields[contentTypeUid]); + referencePaths = referencePaths.filter(function (field) { + return !!field; + }); + paths = this.identifyReferences(receivedData, [], referencePaths); + this.statusStorage.set(contentTypeUid, status); + if (status.hasLivePreviewEntryFound) { + _context5.next = 31; + break; + } + return _context5.abrupt("return", receivedData); + case 31: + _context5.next = 33; + return this.fetchEntry(entryUid, contentTypeUid, paths, (_this$jsonRteFields$c = this.jsonRteFields[contentTypeUid]) !== null && _this$jsonRteFields$c !== void 0 ? _this$jsonRteFields$c : []); + case 33: + entry = _context5.sent; + return _context5.abrupt("return", entry); + case 35: + case "end": + return _context5.stop(); + } + }, _callee5, this, [[8, 20]]); + })); + function getUsingTypeName(_x5) { + return _getUsingTypeName.apply(this, arguments); + } + return getUsingTypeName; + }() + }, { + key: "get", + value: function () { + var _get = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee6(data) { + var _this$stackSdk$live_p, + _this5 = this; + var dataCloned, hasTypeNameAndUid, hasCslpMetaAtRoot, multipleEntriesKey, hasSingleEntry, hasMultipleEntries, receivedData, live_preview, hasCslpMeta, hasMultipleCslpMeta, multipleLPEntries, result, _entryCslpMeta$refere, _entryCslpMeta$jsonRt, entryCslpMeta, contentTypeUid, entryUid, refPaths, rtePaths, entry; + return _regenerator["default"].wrap(function _callee6$(_context6) { + while (1) switch (_context6.prev = _context6.next) { + case 0: + if (!(this.stackSdk.live_preview && !((_this$stackSdk$live_p = this.stackSdk.live_preview) !== null && _this$stackSdk$live_p !== void 0 && _this$stackSdk$live_p.enable))) { + _context6.next = 3; + break; + } + console.warn("Contentstack Gatsby (Live Preview): live preview is disabled in config"); + return _context6.abrupt("return", data); + case 3: + if (!(data === null)) { + _context6.next = 6; + break; + } + console.warn("Contentstack Gatsby (Live Preview): null was passed to get()"); + return _context6.abrupt("return", data); + case 6: + if (this.isNested(data)) { + _context6.next = 9; + break; + } + console.warn("Contentstack Gatsby (Live Preview): data passed to get() is invalid"); + return _context6.abrupt("return", data); + case 9: + dataCloned = (0, _lodash2["default"])(data); + delete dataCloned["$"]; + + // old method metadata + hasTypeNameAndUid = dataCloned.uid && dataCloned.__typename; // new method metadata + hasCslpMetaAtRoot = dataCloned.cslp__meta; + multipleEntriesKey = Object.keys(dataCloned); + hasSingleEntry = !hasCslpMetaAtRoot && multipleEntriesKey.length === 1; + hasMultipleEntries = !hasCslpMetaAtRoot && multipleEntriesKey.length > 1; + receivedData = hasSingleEntry || hasMultipleEntries ? this.unwrapEntryData(dataCloned) : dataCloned; + live_preview = this.stackSdk.live_preview; + if (!(live_preview !== null && live_preview !== void 0 && live_preview.hash && live_preview.hash !== 'init')) { + _context6.next = 59; + break; + } + this.livePreviewConfig = live_preview; + hasCslpMeta = receivedData.cslp__meta; // item can be null, since gatsby can return null + hasMultipleCslpMeta = Array.isArray(receivedData) && receivedData.length > 1 && receivedData.every(function (item) { + return item === null || item.cslp__meta; + }); + if (!(!hasCslpMeta && !hasMultipleCslpMeta && !hasTypeNameAndUid)) { + _context6.next = 24; + break; + } + throw new Error("Contentstack Gatsby (Live Preview): Entry data must contain cslp__meta for live preview"); + case 24: + if (!hasMultipleCslpMeta) { + _context6.next = 40; + break; + } + _context6.prev = 25; + _context6.next = 28; + return Promise.all(receivedData.map(function (item) { + if (item === null) { + return Promise.resolve(null); + } + return _this5.fetchEntry(item.cslp__meta.entryUid, item.cslp__meta.contentTypeUid, item.cslp__meta.refPaths, item.cslp__meta.rtePaths); + })); + case 28: + multipleLPEntries = _context6.sent; + result = {}; + multipleEntriesKey.forEach(function (key, index) { + result[key] = multipleLPEntries[index]; + }); + return _context6.abrupt("return", result); + case 34: + _context6.prev = 34; + _context6.t0 = _context6["catch"](25); + console.error("Contentstack Gatsby (Live Preview):", _context6.t0); + return _context6.abrupt("return", dataCloned); + case 38: + _context6.next = 59; + break; + case 40: + if (!hasCslpMeta) { + _context6.next = 55; + break; + } + entryCslpMeta = receivedData.cslp__meta; + contentTypeUid = entryCslpMeta.contentTypeUid; + entryUid = entryCslpMeta.entryUid; + if (!(!entryUid || !contentTypeUid)) { + _context6.next = 47; + break; + } + console.warn("Contentstack Gatsby (Live Preview): no entry uid or content type uid was found inside cslp__meta"); + return _context6.abrupt("return", dataCloned); + case 47: + refPaths = (_entryCslpMeta$refere = entryCslpMeta.referencePaths) !== null && _entryCslpMeta$refere !== void 0 ? _entryCslpMeta$refere : []; + rtePaths = (_entryCslpMeta$jsonRt = entryCslpMeta.jsonRtePaths) !== null && _entryCslpMeta$jsonRt !== void 0 ? _entryCslpMeta$jsonRt : []; + _context6.next = 51; + return this.fetchEntry(entryUid, contentTypeUid, refPaths, rtePaths); + case 51: + entry = _context6.sent; + return _context6.abrupt("return", entry); + case 55: + if (!hasTypeNameAndUid) { + _context6.next = 59; + break; + } + _context6.next = 58; + return this.getUsingTypeName(dataCloned); + case 58: + return _context6.abrupt("return", _context6.sent); + case 59: + return _context6.abrupt("return", dataCloned); + case 60: + case "end": + return _context6.stop(); + } + }, _callee6, this, [[25, 34]]); + })); + function get(_x6) { + return _get.apply(this, arguments); + } + return get; + }() + }], [{ + key: "addContentTypeUidFromTypename", + value: function addContentTypeUidFromTypename(entry) { + if (typeof entry === "undefined") { + throw new TypeError("entry cannot be empty"); + } + if (entry === null) { + throw new TypeError("entry cannot be null"); + } + if ((0, _typeof2["default"])(entry) !== "object") { + throw new TypeError("entry must be an object"); + } + if (Array.isArray(entry)) { + throw new TypeError("entry cannot be an object, pass an instance of entry"); + } + traverse(entry); + function traverse(field) { + if (!field || (0, _typeof2["default"])(field) !== "object") { + return; + } + if (Array.isArray(field)) { + field.forEach(function (instance) { + return traverse(instance); + }); + } + if (Object.hasOwnProperty.call(field, "__typename") && typeof field.__typename == "string") { + field._content_type_uid = field.__typename.split("_").slice(1).join("_"); + } + Object.values(field).forEach(function (subField) { + return traverse(subField); + }); + } + } + }]); + return ContentstackGatsby; +}(); +exports.ContentstackGatsby = ContentstackGatsby; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/live-preview/resolveCslpMeta.js b/live-preview/resolveCslpMeta.js new file mode 100644 index 0000000..c93e63f --- /dev/null +++ b/live-preview/resolveCslpMeta.js @@ -0,0 +1,103 @@ +"use strict"; + +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.resolveCslpMeta = resolveCslpMeta; +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); +var _getCslpMetaPaths = require("./getCslpMetaPaths"); +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function resolveCslpMeta(_ref) { + var _fieldNode$name, _fieldNode$name$loc, _fieldNode$name2, _fieldNode$name2$loc, _info$fragments; + var source = _ref.source, + _args = _ref.args, + _context = _ref.context, + info = _ref.info, + contentTypeMap = _ref.contentTypeMap, + typePrefix = _ref.typePrefix; + var entryUid = source.uid; + var contentTypeNodeType = source.internal.type; + var queryContentTypeUid = contentTypeNodeType.replace("".concat(typePrefix, "_"), ""); + var fieldNode = info.fieldNodes.find(function (node) { + var _node$name; + return ((_node$name = node.name) === null || _node$name === void 0 ? void 0 : _node$name.value) === "cslp__meta"; + }); + var fieldNodeValue = "cslp__meta"; + var fieldNodeLocation = { + start: (_fieldNode$name = fieldNode.name) === null || _fieldNode$name === void 0 ? void 0 : (_fieldNode$name$loc = _fieldNode$name.loc) === null || _fieldNode$name$loc === void 0 ? void 0 : _fieldNode$name$loc.start, + end: (_fieldNode$name2 = fieldNode.name) === null || _fieldNode$name2 === void 0 ? void 0 : (_fieldNode$name2$loc = _fieldNode$name2.loc) === null || _fieldNode$name2$loc === void 0 ? void 0 : _fieldNode$name2$loc.end + }; + + // We have all the query selections (`info.operation.selectionSet.selections`) + // each time we resolve cslp__meta. + // So, get the correct Selection from query for the current cslp__meta + var queryContentTypeSelection = findQuerySelection(info.operation.selectionSet, fieldNodeValue, fieldNodeLocation); + if (typeof queryContentTypeSelection === "undefined") { + return { + error: { + message: "failed to find query selection for cslp__meta" + } + }; + } + var fragments = (_info$fragments = info === null || info === void 0 ? void 0 : info.fragments) !== null && _info$fragments !== void 0 ? _info$fragments : {}; + var contentType = contentTypeMap[queryContentTypeUid]; + // for the content type selection, get the reference and json RTE paths + var metaPaths = (0, _getCslpMetaPaths.getCslpMetaPaths)(queryContentTypeSelection, "", contentType, fragments, contentTypeMap, typePrefix); + var result = _objectSpread({ + entryUid: entryUid, + contentTypeUid: contentType.uid + }, metaPaths); + return result; +} +function findQuerySelection(selectionSet, value, location) { + var depth = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; + // cslp__meta can only be one level deep (or two level deep, see all* case below) + // e.g. + // query { + // page { + // cslp__meta + // } + // allBlog { + // nodes { + // cslp__meta + // } + // } + // } + if (depth > 1 || !selectionSet || !selectionSet.selections) { + return; + } + var _iterator = _createForOfIteratorHelper(selectionSet.selections), + _step; + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _selection$name, _selection$loc, _selection$loc2, _selection$name2; + var selection = _step.value; + if (((_selection$name = selection.name) === null || _selection$name === void 0 ? void 0 : _selection$name.value) === value && ((_selection$loc = selection.loc) === null || _selection$loc === void 0 ? void 0 : _selection$loc.start) === location.start && ((_selection$loc2 = selection.loc) === null || _selection$loc2 === void 0 ? void 0 : _selection$loc2.end) === location.end) { + return selectionSet; + } + // "nodes" in all* queries will lead to cslp__meta at depth 2 + if (((_selection$name2 = selection.name) === null || _selection$name2 === void 0 ? void 0 : _selection$name2.value) === "nodes") { + var _nestedSelectionSet = findQuerySelection(selection.selectionSet, value, location, depth); + if (_nestedSelectionSet) { + return _nestedSelectionSet; + } + } + // search one level deeper for the correct node in this selection + var nestedSelectionSet = findQuerySelection(selection.selectionSet, value, location, depth + 1); + // return when not undefined, meaning the correct selection has been found + if (nestedSelectionSet) { + return nestedSelectionSet; + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } +} +//# sourceMappingURL=resolveCslpMeta.js.map \ No newline at end of file diff --git a/live-preview/storage-helper.js b/live-preview/storage-helper.js new file mode 100644 index 0000000..9183123 --- /dev/null +++ b/live-preview/storage-helper.js @@ -0,0 +1,59 @@ +"use strict"; + +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Storage = void 0; +var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); +var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); +// This is a helper class to help store key-value data inside +// an individual Storage object's key. +// It simply performs the JSON parsing and stringifying steps +var Storage = /*#__PURE__*/function () { + /** + * @param {string} name + * @param {Storage.prototype} storage + */ + function Storage(storage, name) { + (0, _classCallCheck2["default"])(this, Storage); + (0, _defineProperty2["default"])(this, "storage", void 0); + (0, _defineProperty2["default"])(this, "name", void 0); + if (!(storage !== null && storage !== void 0 && storage.__proto__) === Storage.prototype) { + throw new Error("storage should implment Web Storage API"); + } + this.storage = storage; + this.name = "cslp_" + name; + var stored = this.storage.getItem(this.name); + if (!stored) { + this.storage.setItem(this.name, "{}"); + } + } + (0, _createClass2["default"])(Storage, [{ + key: "getArea", + value: function getArea() { + var area = JSON.parse(this.storage.getItem(this.name)); + return area; + } + }, { + key: "set", + value: function set(key, value) { + var area = this.getArea(); + area[key] = value; + this.storage.setItem(this.name, JSON.stringify(area)); + } + }, { + key: "get", + value: function get(key) { + if (!key) { + return this.getArea(); + } + var area = this.getArea(); + return area[key]; + } + }]); + return Storage; +}(); +exports.Storage = Storage; +//# sourceMappingURL=storage-helper.js.map \ No newline at end of file diff --git a/node-helper.js b/node-helper.js index e95f542..690e47b 100644 --- a/node-helper.js +++ b/node-helper.js @@ -10,11 +10,16 @@ */ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } var preferDefault = function preferDefault(m) { return m && m["default"] || m; }; var fetch = preferDefault(require('node-fetch')); +var _require = require('./utils'), + getCustomHeaders = _require.getCustomHeaders; var deleteContentstackNodes = function deleteContentstackNodes(item, type, createNodeId, getNode, deleteNode, typePrefix) { var nodeId = ''; var node = null; @@ -44,11 +49,11 @@ var validateContentstackAccess = /*#__PURE__*/function () { host = pluginOptions.cdn ? pluginOptions.cdn : 'https://cdn.contentstack.io/v3'; _context.next = 5; return fetch("".concat(host, "/content_types?include_count=false"), { - headers: { + headers: _objectSpread({ api_key: "".concat(pluginOptions.api_key), access_token: "".concat(pluginOptions.delivery_token), branch: pluginOptions === null || pluginOptions === void 0 ? void 0 : pluginOptions.branch - } + }, getCustomHeaders(pluginOptions === null || pluginOptions === void 0 ? void 0 : pluginOptions.enableEarlyAccessKey, pluginOptions === null || pluginOptions === void 0 ? void 0 : pluginOptions.enableEarlyAccessValue)) }).then(function (res) { return res.ok; }).then(function (ok) { diff --git a/plugin-options-schema.js b/plugin-options-schema.js index e41cc99..18945ad 100644 --- a/plugin-options-schema.js +++ b/plugin-options-schema.js @@ -21,8 +21,8 @@ exports.pluginOptionsSchema = function (_ref) { jsonRteToHtml: Joi["boolean"]()["default"](false).description("Specify true if you want to generate html from json RTE field"), httpRetries: Joi.number().integer()["default"](3).description("Specify the number of times to perform http request on a network failure"), limit: Joi.number().integer()["default"](50).description("Specify the number of entries/assets to be fetched per page"), - enableEarlyAccessKey: Joi.string().description("Specify the Header key to be passed to Contentstack API"), - enableEarlyAccessValue: Joi.array().items(Joi.string().required()).description("Specify list of headers to be passed to Contentstack API"), + enableEarlyAccessKey: Joi.string()["default"]('').description("Specify the Header key to be passed to Contentstack API"), + enableEarlyAccessValue: Joi.string()["default"]('').description("Specify list of headers to be passed to Contentstack API.") }).external(validateContentstackAccess); }; //# sourceMappingURL=plugin-options-schema.js.map \ No newline at end of file diff --git a/tests/getHeaders.test.js b/tests/getHeaders.test.js new file mode 100644 index 0000000..7cb2584 --- /dev/null +++ b/tests/getHeaders.test.js @@ -0,0 +1,31 @@ +"use strict"; + +var _require = require('../utils'), + getCustomHeaders = _require.getCustomHeaders; +describe('getCustomHeaders', function () { + test('Check if the Header Key and Value in config are invalid', function () { + expect(getCustomHeaders('', '')).toEqual({}); + expect(getCustomHeaders(undefined, undefined)).toEqual({}); + expect(getCustomHeaders('x-header-ea', '')).toEqual({}); + expect(getCustomHeaders('', 'newcda')).toEqual({}); + expect(getCustomHeaders(' ', ' ')).toEqual({}); + expect(getCustomHeaders(' ', ' ')).toEqual({}); + expect(getCustomHeaders(null, ' ')).toEqual({}); + expect(getCustomHeaders('header-ea', 'cda')).toEqual({}); + }); + test('Check if the Header Key and Value in config are valid', function () { + expect(getCustomHeaders('x-header-ea', 'newcda')).toEqual({ + 'x-header-ea': 'newcda' + }); + expect(getCustomHeaders(' x-header-ea ', ' newcda ')).toEqual({ + 'x-header-ea': 'newcda' + }); + expect(getCustomHeaders('x-header-ea', 'newcda,taxonomy')).toEqual({ + 'x-header-ea': 'newcda,taxonomy' + }); + expect(getCustomHeaders('x-header-ea', ' newcda , taxonomy ')).toEqual({ + 'x-header-ea': 'newcda,taxonomy' + }); + }); +}); +//# sourceMappingURL=getHeaders.test.js.map \ No newline at end of file diff --git a/utils.js b/utils.js index c193ded..21a20af 100644 --- a/utils.js +++ b/utils.js @@ -1,5 +1,7 @@ 'use strict'; +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var ProgressBar = require('progress'); exports.createProgress = function (message, reporter) { if (reporter && reporter.createProgress) { @@ -63,4 +65,12 @@ exports.getContentTypeOption = function (configOptions) { exports.getJSONToHtmlRequired = function (jsonRteToHtml, field) { return jsonRteToHtml && field.field_metadata && field.field_metadata.allow_json_rte; }; +exports.getCustomHeaders = function (key, value) { + var sanitizedKey = typeof key === 'string' ? key.trim() : ''; + var sanitizedValue = typeof value === 'string' ? value.trim() : ''; + if (!sanitizedKey || !sanitizedValue || !sanitizedKey.startsWith('x-')) { + return {}; + } + return (0, _defineProperty2["default"])({}, sanitizedKey, sanitizedValue.replace(/\s/g, '')); +}; //# sourceMappingURL=utils.js.map \ No newline at end of file From 2819967e701ce8ca8234f489e9732c49144de1b1 Mon Sep 17 00:00:00 2001 From: Faraaz Biyabani Date: Wed, 17 Jan 2024 12:35:26 +0530 Subject: [PATCH 8/8] 5.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca576ba..5862a8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gatsby-source-contentstack", - "version": "5.1.3", + "version": "5.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gatsby-source-contentstack", - "version": "5.1.3", + "version": "5.2.0", "license": "MIT", "dependencies": { "@contentstack/utils": "^1.1.3", diff --git a/package.json b/package.json index 463275e..724dc1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gatsby-source-contentstack", - "version": "5.1.3", + "version": "5.2.0", "description": "Gatsby source plugin for building websites using Contentstack as a data source", "scripts": { "prepublish": "npm run build",