From fa0e8c84f59c2a1b0112cc0c76360e032d39a1e1 Mon Sep 17 00:00:00 2001 From: Sven Schmidt <0xsven@gmail.com> Date: Thu, 9 Jul 2020 11:36:28 +0200 Subject: [PATCH] Contentful integration --- .../ContentfulDeliveryApi.credentials.ts | 22 +++ .../nodes/Contentful/ GenericFunctions.ts | 34 ++++ .../nodes/Contentful/AssetDescription.ts | 49 ++++++ .../Contentful/ContentTypeDescription.ts | 49 ++++++ .../nodes/Contentful/Contentful.node.ts | 142 ++++++++++++++++ .../nodes/Contentful/EntryDescription.ts | 83 ++++++++++ .../nodes/Contentful/LocaleDescription.ts | 29 ++++ .../Contentful/SearchParameterDescription.ts | 37 +++++ .../nodes/Contentful/SpaceDescription.ts | 29 ++++ .../nodes/Contentful/contentful.png | Bin 0 -> 4281 bytes .../nodes/Contentful/resolveResponse.ts | 156 ++++++++++++++++++ packages/nodes-base/package.json | 2 + 12 files changed, 632 insertions(+) create mode 100644 packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Contentful/ GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Contentful/AssetDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/Contentful.node.ts create mode 100644 packages/nodes-base/nodes/Contentful/EntryDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/LocaleDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/SpaceDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/contentful.png create mode 100644 packages/nodes-base/nodes/Contentful/resolveResponse.ts diff --git a/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts b/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts new file mode 100644 index 0000000000..ce620fcdbb --- /dev/null +++ b/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts @@ -0,0 +1,22 @@ +import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; + +export class ContentfulDeliveryApi implements ICredentialType { + name = 'contentfulDeliveryApi'; + displayName = 'Delivery API'; + properties = [ + { + displayName: 'Space Id', + name: 'space_id', + type: 'string' as NodePropertyTypes, + default: '', + description: 'The id for the Cotentful space.' + }, + { + displayName: 'Access Token', + name: 'access_token', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Access token that has access to the space' + } + ]; +} diff --git a/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts b/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts new file mode 100644 index 0000000000..c59bab1449 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts @@ -0,0 +1,34 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { OptionsWithUrl } from 'request'; + +/** + * @param {IExecuteFunctions} that Reference to the system's execute functions + * @param {string} endpoint? Endpoint of api call + * @param {string} environmentId? Id of contentful environment (eg. master, staging, etc.) + * @param {Record} qs? Query string, can be used for search parameters + */ +export const contentfulApiRequest = async ( + that: IExecuteFunctions, + endpoint?: string, + environmentId?: string, + qs?: Record +) => { + const credentials = that.getCredentials('contentfulDeliveryApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let url = `https://cdn.contentful.com/spaces/${credentials.space_id}`; + if (environmentId) url = `${url}/environments/${environmentId}`; + if (endpoint) url = `${url}${endpoint}`; + qs = qs || {}; + qs.access_token = credentials.access_token as string; + + const res = await that.helpers.request!({ + url, + method: 'GET', + qs + } as OptionsWithUrl); + + return JSON.parse(res); +}; diff --git a/packages/nodes-base/nodes/Contentful/AssetDescription.ts b/packages/nodes-base/nodes/Contentful/AssetDescription.ts new file mode 100644 index 0000000000..18ccba8257 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/AssetDescription.ts @@ -0,0 +1,49 @@ +import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; + +export const resource = { + name: "Asset", + value: "asset", +} as INodePropertyOptions; + +export const operations = [ + { + displayName: "Operation", + name: "operation", + type: "options", + displayOptions: { + show: { + resource: [resource.value], + }, + }, + options: [ + { + name: "Get Assets", + value: "get_assets", + }, + { + name: "Get Single Asset", + value: "get_asset", + }, + ], + default: "get_assets", + description: "The operation to perform.", + }, +] as INodeProperties[]; + +export const fields = [ + { + displayName: "Asset Id", + name: "asset_id", + type: "string", + default: "", + placeholder: "", + description: "", + required: true, + displayOptions: { + show: { + resource: [resource.value], + operation: ["get_asset"], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts b/packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts new file mode 100644 index 0000000000..0a2ebfd1e7 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts @@ -0,0 +1,49 @@ +import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; + +export const resource = { + name: "Content Types", + value: "content_type", +} as INodePropertyOptions; + +export const operations = [ + { + displayName: "Operation", + name: "operation", + type: "options", + displayOptions: { + show: { + resource: [resource.value], + }, + }, + options: [ + { + name: "Get Content types", + value: "get_content_types", + }, + { + name: "Get Single Content Type", + value: "get_content_type", + }, + ], + default: "get_content_types", + description: "The operation to perform.", + }, +] as INodeProperties[]; + +export const fields = [ + { + displayName: "Content Type Id", + name: "content_type_id", + type: "string", + default: "", + placeholder: "", + description: "", + required: true, + displayOptions: { + show: { + resource: [resource.value], + operation: ["get_content_type"], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/Contentful.node.ts b/packages/nodes-base/nodes/Contentful/Contentful.node.ts new file mode 100644 index 0000000000..866cddb268 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/Contentful.node.ts @@ -0,0 +1,142 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, NodePropertyTypes } from 'n8n-workflow'; + +import { contentfulApiRequest } from './ GenericFunctions'; +import resolveResponse from './resolveResponse'; + +import * as SpaceDescription from './SpaceDescription'; +import * as ContentTypeDescription from './ContentTypeDescription'; +import * as EntryDescription from './EntryDescription'; +import * as AssetDescription from './AssetDescription'; +import * as LocaleDescription from './LocaleDescription'; +import * as SearchParameterDescription from './SearchParameterDescription'; + +export class Contentful implements INodeType { + description: INodeTypeDescription = { + displayName: 'Contentful', + name: 'contentful', + icon: 'file:contentful.png', + group: ['input'], + version: 1, + description: "Access data through Contentful's Content Delivery API", + defaults: { + name: 'Contentful', + color: '#2E75D4' + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'contentfulDeliveryApi', + required: true + } + ], + properties: [ + // Common fields: + { + displayName: 'Environment Id', + name: 'environment_id', + type: 'string' as NodePropertyTypes, + default: 'master', + description: + 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".' + }, + + // Resources: + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + SpaceDescription.resource, + ContentTypeDescription.resource, + EntryDescription.resource, + AssetDescription.resource, + LocaleDescription.resource + ], + default: '', + description: 'The resource to operate on.' + }, + + // Operations: + ...SpaceDescription.operations, + ...ContentTypeDescription.operations, + ...EntryDescription.operations, + ...AssetDescription.operations, + ...LocaleDescription.operations, + + // Resource specific fields: + ...SpaceDescription.fields, + ...ContentTypeDescription.fields, + ...EntryDescription.fields, + ...AssetDescription.fields, + ...LocaleDescription.fields, + + // Options: + ...SearchParameterDescription.fields + ] + }; + + async execute(this: IExecuteFunctions): Promise { + const environmentId = this.getNodeParameter('environment_id', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const qs: Record = {}; + + for (let i = 0; i < items.length; i++) { + if (resource === 'space') { + if (operation === 'get_space') { + const res = await contentfulApiRequest(this); + returnData.push(res); + } + } else if (resource === 'content_type') { + if (operation === 'get_content_types') { + const res = await contentfulApiRequest(this, '/content_types', environmentId); + const resolvedData = resolveResponse(res, {}); + returnData.push(...resolvedData); + } else if (operation === 'get_content_type') { + const id = this.getNodeParameter('content_type_id', 0) as string; + const res = await contentfulApiRequest(this, `/content_types/${id}`, environmentId); + returnData.push(...res.items); + } + } else if (resource === 'entry') { + if (operation === 'get_entries') { + const shouldResolve = this.getNodeParameter('resolve', 0) as boolean; + if (shouldResolve) qs.include = this.getNodeParameter('include', 0) as number; + const searchParameters = this.getNodeParameter('search_parameters', 0) as IDataObject; + if (searchParameters.parameters && Array.isArray(searchParameters.parameters)) { + searchParameters.parameters.forEach(parameter => { + const { name, value } = parameter as { name: string; value: string }; + qs[name] = value; + }); + } + const res = await contentfulApiRequest(this, '/entries', environmentId, qs); + const resolvedData = shouldResolve ? resolveResponse(res, {}) : res.items; + returnData.push(...resolvedData); + } else if (operation === 'get_entry') { + const id = this.getNodeParameter('entry_id', 0) as string; + const res = await contentfulApiRequest(this, `/entries/${id}`, environmentId); + returnData.push(res); + } + } else if (resource === 'asset') { + if (operation === 'get_assets') { + const res = await contentfulApiRequest(this, '/assets', environmentId); + returnData.push(...res.items); + } else if (operation === 'get_asset') { + const id = this.getNodeParameter('asset_id', 0) as string; + const res = await contentfulApiRequest(this, `/assets/${id}`, environmentId); + returnData.push(res); + } + } else if (resource === 'locale') { + if (operation === 'get_locales') { + const res = await contentfulApiRequest(this, '/locales', environmentId); + returnData.push(res); + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Contentful/EntryDescription.ts b/packages/nodes-base/nodes/Contentful/EntryDescription.ts new file mode 100644 index 0000000000..781bcc6fe1 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/EntryDescription.ts @@ -0,0 +1,83 @@ +import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; + +export const resource = { + name: 'Entry', + value: 'entry' +} as INodePropertyOptions; + +export const operations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [resource.value] + } + }, + options: [ + { + name: 'Get Entries', + value: 'get_entries' + }, + { + name: 'Get Single Entry', + value: 'get_entry' + } + ], + default: 'get_entries', + description: 'The operation to perform.' + } +] as INodeProperties[]; + +export const fields = [ + { + displayName: 'Resolve', + name: 'resolve', + type: 'boolean', + default: false, + description: 'Linked entries can be automatically resolved in the results if you click activate this feature.', + displayOptions: { + show: { + resource: [resource.value], + operation: ['get_entries'] + } + } + }, + { + displayName: 'Include', + name: 'include', + type: 'number', + default: 1, + placeholder: '', + description: + "When you have related content (e.g. entries with links to image assets) it's possible to include them in the results. Using the include parameter, you can specify the number of levels of entries to include in the results. A lower number might improve performance.", + typeOptions: { + minValue: 0, + maxValue: 10 + }, + displayOptions: { + show: { + resource: [resource.value], + operation: ['get_entries'], + resolve: [true] + } + } + }, + + { + displayName: 'Entry Id', + name: 'entry_id', + type: 'string', + default: '', + placeholder: '', + description: '', + required: true, + displayOptions: { + show: { + resource: [resource.value], + operation: ['get_entry'] + } + } + } +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/LocaleDescription.ts b/packages/nodes-base/nodes/Contentful/LocaleDescription.ts new file mode 100644 index 0000000000..87e7f6b932 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/LocaleDescription.ts @@ -0,0 +1,29 @@ +import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; + +export const resource = { + name: "Locale", + value: "locale", +} as INodePropertyOptions; + +export const operations = [ + { + displayName: "Operation", + name: "operation", + type: "options", + displayOptions: { + show: { + resource: [resource.value], + }, + }, + options: [ + { + name: "Get Locales", + value: "get_locales", + }, + ], + default: "get_locales", + description: "The operation to perform.", + }, +] as INodeProperties[]; + +export const fields = [] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts b/packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts new file mode 100644 index 0000000000..7557cbb755 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts @@ -0,0 +1,37 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const fields = [ + { + displayName: 'Search Parameters', + name: 'search_parameters', + description: 'You can use a variety of query parameters to search and filter items.', + placeholder: 'Add parameter', + type: 'fixedCollection', + typeOptions: { + multipleValues: true + }, + default: {}, + options: [ + { + displayName: 'Parameters', + name: 'parameters', + values: [ + { + displayName: 'Parameter Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the search parameter to set.' + }, + { + displayName: 'Parameter Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the search parameter to set.' + } + ] + } + ] + } +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/SpaceDescription.ts b/packages/nodes-base/nodes/Contentful/SpaceDescription.ts new file mode 100644 index 0000000000..2baabee099 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/SpaceDescription.ts @@ -0,0 +1,29 @@ +import { INodeProperties } from "n8n-workflow"; + +export const resource = { + name: "Space", + value: "space", +}; + +export const operations = [ + { + displayName: "Operation", + name: "operation", + type: "options", + displayOptions: { + show: { + resource: [resource.value], + }, + }, + options: [ + { + name: "Get Space", + value: "get_space", + }, + ], + default: "get_space", + description: "The operation to perform.", + }, +] as INodeProperties[]; + +export const fields = [] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/contentful.png b/packages/nodes-base/nodes/Contentful/contentful.png new file mode 100644 index 0000000000000000000000000000000000000000..4448da65ae1c71a2243e4fd6545568a6b1d54358 GIT binary patch literal 4281 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 PivW< + object && object.sys && object.sys.type === 'Link'; + +/** + * findNormalizableLinkInArray + * + * @param array + * @param predicate + * @return {*} + */ +const findNormalizableLinkInArray = (array, predicate) => { + for (let i = 0, len = array.length; i < len; i++) { + if (predicate(array[i])) { + return array[i]; + } + } + return UNRESOLVED_LINK; +}; + +/** + * getLink Function + * + * @param response + * @param link + * @return {undefined} + */ +const getLink = (allEntries, link) => { + const { linkType: type, id } = link.sys; + + const predicate = ({ sys }) => sys.type === type && sys.id === id; + + return findNormalizableLinkInArray(allEntries, predicate); +}; + +/** + * cleanUpLinks Function + * - Removes unresolvable links from Arrays and Objects + * + * @param {Object[]|Object} input + */ +const cleanUpLinks = input => { + if (Array.isArray(input)) { + return input.filter(val => val !== UNRESOLVED_LINK); + } + for (const key in input) { + if (input[key] === UNRESOLVED_LINK) { + delete input[key]; + } + } + return input; +}; + +/** + * walkMutate Function + * @param input + * @param predicate + * @param mutator + * @return {*} + */ +const walkMutate = (input, predicate, mutator, removeUnresolved) => { + if (predicate(input)) { + return mutator(input); + } + + if (input && typeof input === 'object') { + for (const key in input) { + if (input.hasOwnProperty(key)) { + input[key] = walkMutate( + input[key], + predicate, + mutator, + removeUnresolved + ); + } + } + if (removeUnresolved) { + input = cleanUpLinks(input); + } + } + return input; +}; + +const normalizeLink = (allEntries, link, removeUnresolved) => { + const resolvedLink = getLink(allEntries, link); + if (resolvedLink === UNRESOLVED_LINK) { + return removeUnresolved ? resolvedLink : link; + } + return resolvedLink; +}; + +const makeEntryObject = (item, itemEntryPoints) => { + if (!Array.isArray(itemEntryPoints)) { + return item; + } + + const entryPoints = Object.keys(item).filter( + ownKey => itemEntryPoints.indexOf(ownKey) !== -1 + ); + + return entryPoints.reduce((entryObj, entryPoint) => { + entryObj[entryPoint] = item[entryPoint]; + return entryObj; + }, {}); +}; + +/** + * resolveResponse Function + * Resolves contentful response to normalized form. + * @param {Object} response Contentful response + * @param {Object} options + * @param {Boolean} options.removeUnresolved - Remove unresolved links default:false + * @param {Array} options.itemEntryPoints - Resolve links only in those item properties + * @return {Object} + */ +const resolveResponse = (response, options) => { + options = options || {}; + if (!response.items) { + return []; + } + const responseClone = cloneDeep(response); + const allIncludes = Object.keys(responseClone.includes || {}).reduce( + (all, type) => [...all, ...response.includes[type]], + [] + ); + + const allEntries = [...responseClone.items, ...allIncludes]; + + allEntries.forEach(item => { + const entryObject = makeEntryObject(item, options.itemEntryPoints); + + Object.assign( + item, + walkMutate( + entryObject, + isLink, + link => normalizeLink(allEntries, link, options.removeUnresolved), + options.removeUnresolved + ) + ); + }); + + return responseClone.items; +}; + +export default resolveResponse; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 37b9adecf2..45b703b9d8 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -43,6 +43,7 @@ "dist/credentials/ClockifyApi.credentials.js", "dist/credentials/CockpitApi.credentials.js", "dist/credentials/CodaApi.credentials.js", + "dist/credentials/ContentfulDeliveryApi.credentials.js", "dist/credentials/CopperApi.credentials.js", "dist/credentials/CalendlyApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", @@ -159,6 +160,7 @@ "dist/nodes/Clockify/ClockifyTrigger.node.js", "dist/nodes/Cockpit/Cockpit.node.js", "dist/nodes/Coda/Coda.node.js", + "dist/nodes/Contentful/Contentful.node.js", "dist/nodes/Copper/CopperTrigger.node.js", "dist/nodes/Cron.node.js", "dist/nodes/Crypto.node.js",