Improvements to Contenful-Node

This commit is contained in:
ricardo 2020-08-04 15:07:54 -04:00
parent 6573642fc1
commit bc0b349faa
11 changed files with 810 additions and 463 deletions

View file

@ -0,0 +1,36 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
//https://www.contentful.com/developers/docs/references/authentication/
export class ContentfulApi implements ICredentialType {
name = 'contentfulApi';
displayName = 'Contenful API';
properties = [
{
displayName: 'Space ID',
name: 'spaceId',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
description: 'The id for the Contentful space.'
},
{
displayName: 'Content Delivery API Access token',
name: 'ContentDeliveryaccessToken',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
description: 'Access token that has access to the space'
},
{
displayName: 'Content Preview API Access token',
name: 'ContentPreviewaccessToken',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
description: 'Access token that has access to the space'
},
];
}

View file

@ -1,31 +0,0 @@
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: '',
required: true,
description: 'The id for the Contentful space.'
},
{
displayName: 'Content Delivery API - access token',
name: 'access_token',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
description: 'Access token that has access to the space'
},
{
displayName: 'Content Preview API - access token',
name: 'access_token_preview',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Access token that has access to the space'
}
];
}

View file

@ -1,48 +1,196 @@
import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; import {
INodeProperties,
INodePropertyOptions,
} from 'n8n-workflow';
export const resource = { export const resource = {
name: "Asset", name: 'Asset',
value: "asset", value: 'asset',
} as INodePropertyOptions; } as INodePropertyOptions;
export const operations = [ export const operations = [
{ {
displayName: "Operation", displayName: 'Operation',
name: "operation", name: 'operation',
type: "options", type: 'options',
displayOptions: { displayOptions: {
show: { show: {
resource: [resource.value], resource: [
resource.value,
],
}, },
}, },
options: [ options: [
{ {
name: "Get Assets", name: 'Get',
value: "get_assets", value: 'get',
}, },
{ {
name: "Get Single Asset", name: 'Get All',
value: "get_asset", value: 'getAll',
}, },
], ],
default: "get_assets", default: 'getAll',
description: "The operation to perform.", description: 'The operation to perform.',
}, },
] as INodeProperties[]; ] as INodeProperties[];
export const fields = [ export const fields = [
{ {
displayName: "Asset Id", displayName: 'Environment ID',
name: "asset_id", name: 'environmentId',
type: "string", type: 'string',
default: "", displayOptions: {
placeholder: "", show: {
description: "", resource: [
resource.value,
],
operation: [
'get',
'getAll',
],
},
},
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".'
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Equal',
name: 'equal',
type: 'string',
default: '',
placeholder: 'fields.title=n8n',
description: 'Search for all data that matches the condition: {attribute}={value}. Attribute can use dot notation.',
},
{
displayName: 'Exclude',
name: 'exclude',
type: 'string',
default: '',
placeholder: 'fields.tags[nin]=accessories,flowers',
description: 'Search for all data that matches the condition: {attribute}[nin]={value}. Attribute can use dot notation.',
},
{
displayName: 'Exist',
name: 'exist',
type: 'string',
default: '',
placeholder: 'fields.tags[exists]=true',
description: 'Search for all data that matches the condition: {attribute}[exists]={value}. Attribute can use dot notation.',
},
{
displayName: 'Fields',
name: 'select',
type: 'string',
placeholder: 'fields.title',
default: '',
description: 'The select operator allows you to choose what fields to return from an entity. You can choose multiple values by combining comma separated operators.',
},
{
displayName: 'Include',
name: 'include',
type: 'string',
default: '',
placeholder: 'fields.tags[in]=accessories,flowers',
description: 'Search for all data that matches the condition: {attribute}[in]={value}. Attribute can use dot notation.',
},
{
displayName: 'Not Equal',
name: 'notEqual',
type: 'string',
default: '',
placeholder: 'fields.title[ne]=n8n',
description: 'Search for all data that matches the condition: {attribute}[ne]={value}. Attribute can use dot notation.',
},
{
displayName: 'Order',
name: 'order',
type: 'string',
default: '',
placeholder: 'sys.createdAt',
description: 'You can order items in the response by specifying the order search parameter. You can use sys properties (such as sys.createdAt) or field values (such as fields.myCustomDateField) for ordering.',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
description: ' Full-text search is case insensitive and might return more results than expected. A query will only take values with more than 1 character.',
},
],
},
{
displayName: 'Asset ID',
name: 'assetId',
type: 'string',
default: '',
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
resource: [resource.value], resource: [
operation: ["get_asset"], resource.value
],
operation: [
'get',
],
}, },
}, },
}, },

View file

@ -1,48 +1,68 @@
import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; import {
INodeProperties,
INodePropertyOptions,
} from 'n8n-workflow';
export const resource = { export const resource = {
name: "Content Types", name: 'Content Type',
value: "content_type", value: 'contentType',
} as INodePropertyOptions; } as INodePropertyOptions;
export const operations = [ export const operations = [
{ {
displayName: "Operation", displayName: 'Operation',
name: "operation", name: 'operation',
type: "options", type: 'options',
displayOptions: { displayOptions: {
show: { show: {
resource: [resource.value], resource: [
resource.value,
],
}, },
}, },
options: [ options: [
{ {
name: "Get Content types", name: 'Get',
value: "get_content_types", value: 'get',
},
{
name: "Get Single Content Type",
value: "get_content_type",
}, },
], ],
default: "get_content_types", default: 'get',
description: "The operation to perform.", description: 'The operation to perform.',
}, },
] as INodeProperties[]; ] as INodeProperties[];
export const fields = [ export const fields = [
{ {
displayName: "Content Type Id", displayName: 'Environment ID',
name: "content_type_id", name: 'environmentId',
type: "string", type: 'string',
default: "", displayOptions: {
placeholder: "", show: {
description: "", resource: [
resource.value,
],
operation: [
'get',
],
},
},
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".'
},
{
displayName: 'Content Type ID',
name: 'contentTypeId',
type: 'string',
default: '',
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
resource: [resource.value], resource: [
operation: ["get_content_type"], resource.value,
],
operation: [
'get',
],
}, },
}, },
}, },

View file

@ -1,24 +1,34 @@
import { IExecuteFunctions } from 'n8n-core'; import {
import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, NodePropertyTypes } from 'n8n-workflow'; IExecuteFunctions,
} from 'n8n-core';
import { contentfulApiRequest } from './GenericFunctions'; import {
import resolveResponse from './resolveResponse'; IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
contentfulApiRequest,
contenfulApiRequestAllItems,
} from './GenericFunctions';
import * as SpaceDescription from './SpaceDescription'; import * as SpaceDescription from './SpaceDescription';
import * as ContentTypeDescription from './ContentTypeDescription'; import * as ContentTypeDescription from './ContentTypeDescription';
import * as EntryDescription from './EntryDescription'; import * as EntryDescription from './EntryDescription';
import * as AssetDescription from './AssetDescription'; import * as AssetDescription from './AssetDescription';
import * as LocaleDescription from './LocaleDescription'; import * as LocaleDescription from './LocaleDescription';
import * as SearchParameterDescription from './SearchParameterDescription';
export class Contentful implements INodeType { export class Contentful implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Contentful', displayName: 'Contentful',
name: 'contentful', name: 'contentful',
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
icon: 'file:contentful.png', icon: 'file:contentful.png',
group: ['input'], group: ['input'],
version: 1, version: 1,
description: "Access data through Contentful's Content Delivery API", description: 'Consume Contenful API',
defaults: { defaults: {
name: 'Contentful', name: 'Contentful',
color: '#2E75D4' color: '#2E75D4'
@ -27,49 +37,39 @@ export class Contentful implements INodeType {
outputs: ['main'], outputs: ['main'],
credentials: [ credentials: [
{ {
name: 'contentfulDeliveryApi', name: 'contentfulApi',
required: true required: true
} },
], ],
properties: [ properties: [
// Common fields:
{ {
displayName: 'Source', displayName: 'Source',
name: 'source', name: 'source',
type: 'options' as NodePropertyTypes, type: 'options',
default: 'Delivery API', default: 'Delivery API',
description: 'Pick where your data comes from, delivery or preview API', description: 'Pick where your data comes from, delivery or preview API',
options: [ options: [
{ {
name: 'Delivery API', name: 'Delivery API',
value: 'delivery_api' value: 'deliveryApi'
}, },
{ {
name: 'Preview API', name: 'Preview API',
value: 'preview_api' value: 'previewApi'
}
]
}, },
{ ],
displayName: 'Environment Id',
name: 'environment_id',
type: 'string' as NodePropertyTypes,
default: '',
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: // Resources:
{ {
displayName: 'Resource', displayName: 'Resource',
name: 'resource', name: 'resource',
type: 'options', type: 'options',
options: [ options: [
SpaceDescription.resource, AssetDescription.resource,
ContentTypeDescription.resource, ContentTypeDescription.resource,
EntryDescription.resource, EntryDescription.resource,
AssetDescription.resource, LocaleDescription.resource,
LocaleDescription.resource SpaceDescription.resource,
], ],
default: '', default: '',
description: 'The resource to operate on.' description: 'The resource to operate on.'
@ -88,16 +88,13 @@ export class Contentful implements INodeType {
...EntryDescription.fields, ...EntryDescription.fields,
...AssetDescription.fields, ...AssetDescription.fields,
...LocaleDescription.fields, ...LocaleDescription.fields,
],
// Options:
...SearchParameterDescription.fields
]
}; };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const environmentId = this.getNodeParameter('environment_id', 0) as string;
const resource = this.getNodeParameter('resource', 0) as string; const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string; const operation = this.getNodeParameter('operation', 0) as string;
let responseData;
const items = this.getInputData(); const items = this.getInputData();
const returnData: IDataObject[] = []; const returnData: IDataObject[] = [];
@ -105,53 +102,163 @@ export class Contentful implements INodeType {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
if (resource === 'space') { if (resource === 'space') {
if (operation === 'get_space') { if (operation === 'get') {
const res = await contentfulApiRequest(this);
returnData.push(res); const credentials = this.getCredentials('contentfulApi');
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}`);
} }
} 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 (resource === 'contentType') {
if (operation === 'get_entries') { if (operation === 'get') {
const shouldResolve = this.getNodeParameter('resolve', 0) as boolean;
if (shouldResolve) qs.include = this.getNodeParameter('include', 0) as number; const credentials = this.getCredentials('contentfulApi');
const searchParameters = this.getNodeParameter('search_parameters', 0) as IDataObject;
if (searchParameters.parameters && Array.isArray(searchParameters.parameters)) { const env = this.getNodeParameter('environmentId', 0) as string;
searchParameters.parameters.forEach(parameter => {
const { name, value } = parameter as { name: string; value: string }; const id = this.getNodeParameter('contentTypeId', 0) as string;
qs[name] = value;
}); responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/content_types/${id}`);
} }
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 (resource === 'entry') {
if (operation === 'get_assets') {
const res = await contentfulApiRequest(this, '/assets', environmentId); if (operation === 'get') {
returnData.push(...res.items);
} else if (operation === 'get_asset') { const credentials = this.getCredentials('contentfulApi');
const id = this.getNodeParameter('asset_id', 0) as string;
const res = await contentfulApiRequest(this, `/assets/${id}`, environmentId); const env = this.getNodeParameter('environmentId', 0) as string;
returnData.push(res);
const id = this.getNodeParameter('entryId', 0) as string;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/entries/${id}`, {}, qs);
} else if (operation === 'getAll') {
const credentials = this.getCredentials('contentfulApi');
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const env = this.getNodeParameter('environmentId', i) as string;
Object.assign(qs, additionalFields);
if (qs.equal) {
const [atribute, value] = (qs.equal as string).split('=');
qs[atribute] = value;
delete qs.equal;
} }
} else if (resource === 'locale') {
if (operation === 'get_locales') { if (qs.notEqual) {
const res = await contentfulApiRequest(this, '/locales', environmentId); const [atribute, value] = (qs.notEqual as string).split('=');
returnData.push(res); qs[atribute] = value;
delete qs.notEqual;
} }
if (qs.include) {
const [atribute, value] = (qs.include as string).split('=');
qs[atribute] = value;
delete qs.include;
}
if (qs.exclude) {
const [atribute, value] = (qs.exclude as string).split('=');
qs[atribute] = value;
delete qs.exclude;
}
if (returnAll) {
responseData = await contenfulApiRequestAllItems.call(this, 'items', 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/entries`, {}, qs);
} else {
const limit = this.getNodeParameter('limit', 0) as number;
qs.limit = limit;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/entries`, {}, qs);
responseData = responseData.items;
}
}
}
if (resource === 'asset') {
if (operation === 'get') {
const credentials = this.getCredentials('contentfulApi');
const env = this.getNodeParameter('environmentId', 0) as string;
const id = this.getNodeParameter('assetId', 0) as string;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/assets/${id}`, {}, qs);
} else if (operation === 'getAll') {
const credentials = this.getCredentials('contentfulApi');
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const env = this.getNodeParameter('environmentId', i) as string;
Object.assign(qs, additionalFields);
if (qs.equal) {
const [atribute, value] = (qs.equal as string).split('=');
qs[atribute] = value;
delete qs.equal;
}
if (qs.notEqual) {
const [atribute, value] = (qs.notEqual as string).split('=');
qs[atribute] = value;
delete qs.notEqual;
}
if (qs.include) {
const [atribute, value] = (qs.include as string).split('=');
qs[atribute] = value;
delete qs.include;
}
if (qs.exclude) {
const [atribute, value] = (qs.exclude as string).split('=');
qs[atribute] = value;
delete qs.exclude;
}
if (returnAll) {
responseData = await contenfulApiRequestAllItems.call(this, 'items', 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/assets`, {}, qs);
} else {
const limit = this.getNodeParameter('limit', 0) as number;
qs.limit = limit;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/assets`, {}, qs);
responseData = responseData.items;
}
}
}
if (resource === 'locale') {
if (operation === 'getAll') {
const credentials = this.getCredentials('contentfulApi');
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const env = this.getNodeParameter('environmentId', i) as string;
if (returnAll) {
responseData = await contenfulApiRequestAllItems.call(this, 'items', 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/locales`, {}, qs);
} else {
const limit = this.getNodeParameter('limit', 0) as number;
qs.limit = limit;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/locales`, {}, qs);
responseData = responseData.items;
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
} }
} }
return [this.helpers.returnJsonArray(returnData)]; return [this.helpers.returnJsonArray(returnData)];

View file

@ -1,8 +1,14 @@
import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; import {
INodeProperties,
INodePropertyOptions,
} from 'n8n-workflow';
import { type } from 'os';
import { notEqual } from 'assert';
import { exists } from 'fs';
export const resource = { export const resource = {
name: 'Entry', name: 'Entry',
value: 'entry' value: 'entry',
} as INodePropertyOptions; } as INodePropertyOptions;
export const operations = [ export const operations = [
@ -12,72 +18,190 @@ export const operations = [
type: 'options', type: 'options',
displayOptions: { displayOptions: {
show: { show: {
resource: [resource.value] resource: [
} resource.value,
],
},
}, },
options: [ options: [
{ {
name: 'Get Entries', name: 'Get',
value: 'get_entries' value: 'get',
}, },
{ {
name: 'Get Single Entry', name: 'Get All',
value: 'get_entry' value: 'getAll',
} },
], ],
default: 'get_entries', default: 'get',
description: 'The operation to perform.' description: 'The operation to perform.'
} }
] as INodeProperties[]; ] as INodeProperties[];
export const fields = [ export const fields = [
{ {
displayName: 'Resolve', displayName: 'Environment ID',
name: 'resolve', name: 'environmentId',
type: 'boolean', type: 'string',
default: false,
description: 'Linked entries can be automatically resolved in the results if you click activate this feature.',
displayOptions: { displayOptions: {
show: { show: {
resource: [resource.value], resource: [
operation: ['get_entries'] resource.value,
} ],
} operation: [
'get',
'getAll',
],
},
},
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".'
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Content Type ID',
name: 'content_type',
type: 'string',
default: '',
description: 'To search for entries with a specific content type',
},
{
displayName: 'Equal',
name: 'equal',
type: 'string',
default: '',
placeholder: 'fields.title=n8n',
description: 'Search for all data that matches the condition: {attribute}={value}. Attribute can use dot notation.',
},
{
displayName: 'Exclude',
name: 'exclude',
type: 'string',
default: '',
placeholder: 'fields.tags[nin]=accessories,flowers',
description: 'Search for all data that matches the condition: {attribute}[nin]={value}. Attribute can use dot notation.',
},
{
displayName: 'Exist',
name: 'exist',
type: 'string',
default: '',
placeholder: 'fields.tags[exists]=true',
description: 'Search for all data that matches the condition: {attribute}[exists]={value}. Attribute can use dot notation.',
},
{
displayName: 'Fields',
name: 'select',
type: 'string',
placeholder: 'fields.title',
default: '',
description: 'The select operator allows you to choose what fields to return from an entity. You can choose multiple values by combining comma separated operators.',
}, },
{ {
displayName: 'Include', displayName: 'Include',
name: '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', type: 'string',
default: '', default: '',
placeholder: '', placeholder: 'fields.tags[in]=accessories,flowers',
description: '', description: 'Search for all data that matches the condition: {attribute}[in]={value}. Attribute can use dot notation.',
},
{
displayName: 'Not Equal',
name: 'notEqual',
type: 'string',
default: '',
placeholder: 'fields.title[ne]=n8n',
description: 'Search for all data that matches the condition: {attribute}[ne]={value}. Attribute can use dot notation.',
},
{
displayName: 'Order',
name: 'order',
type: 'string',
default: '',
placeholder: 'sys.createdAt',
description: 'You can order items in the response by specifying the order search parameter. You can use sys properties (such as sys.createdAt) or field values (such as fields.myCustomDateField) for ordering.',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
description: ' Full-text search is case insensitive and might return more results than expected. A query will only take values with more than 1 character.',
},
],
},
{
displayName: 'Entry ID',
name: 'entryId',
type: 'string',
default: '',
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
resource: [resource.value], resource: [
operation: ['get_entry'] resource.value,
} ],
} operation: [
'get',
],
},
},
} }
] as INodeProperties[]; ] as INodeProperties[];

View file

@ -1,45 +1,75 @@
import { IExecuteFunctions } from 'n8n-core'; import {
import { OptionsWithUrl } from 'request'; IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
/** import {
* @param {IExecuteFunctions} that Reference to the system's execute functions OptionsWithUri,
* @param {string} endpoint? Endpoint of api call } from 'request';
* @param {string} environmentId? Id of contentful environment (eg. master, staging, etc.)
* @param {Record<string|number>} qs? Query string, can be used for search parameters import {
*/ IDataObject,
export const contentfulApiRequest = async ( } from 'n8n-workflow';
that: IExecuteFunctions,
endpoint?: string, export async function contentfulApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
environmentId?: string,
qs?: Record<string, string | number | undefined> const credentials = this.getCredentials('contentfulApi');
) => {
const credentials = that.getCredentials('contentfulDeliveryApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new Error('No credentials got returned!');
} }
const source = that.getNodeParameter('source', 0) as string; const source = this.getNodeParameter('source', 0) as string;
const isPreview = source === 'preview_api'; const isPreview = source === 'previewApi';
let accessToken = credentials.access_token as string;
if (isPreview) {
accessToken = credentials.access_token_preview as string;
console.log('accessToken', accessToken);
if (!accessToken) {
throw new Error('No access token for preview API set in credentials!');
}
}
let url = `https://${isPreview ? 'preview' : 'cdn'}.contentful.com/spaces/${credentials.space_id}`; const options: OptionsWithUri = {
if (environmentId) url = `${url}/environments/${environmentId}`; method,
if (endpoint) url = `${url}${endpoint}`; qs,
qs = qs || {}; body,
qs.access_token = accessToken; uri: uri ||`https://${isPreview ? 'preview' : 'cdn'}.contentful.com${resource}`,
json: true
const res = await that.helpers.request!({
url,
method: 'GET',
qs
} as OptionsWithUrl);
return JSON.parse(res);
}; };
if (isPreview) {
qs.access_token = credentials.ContentPreviewaccessToken as string;
} else {
qs.access_token = credentials.ContentDeliveryaccessToken as string;
}
try {
return await this.helpers.request!(options);
} catch (error) {
let errorMessage = error;
if (error.response && error.response.body && error.response.body.details) {
const details = error.response.body.details;
errorMessage = details.errors.map((e: IDataObject) => e.details).join('|');
} else if (error.response && error.response.body && error.response.body.message) {
errorMessage = error.response.body.message;
}
throw new Error(`Contentful error response [${error.statusCode}]: ${errorMessage}`);
}
}
export async function contenfulApiRequestAllItems(this: ILoadOptionsFunctions | IExecuteFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.limit = 100;
query.skip = 0;
do {
responseData = await contentfulApiRequest.call(this, method, resource, body, query);
query.skip = (query.skip + 1) * query.limit;
returnData.push.apply(returnData, responseData[propertyName]);
} while (
returnData.length < responseData.total
);
return returnData;
}

View file

@ -1,29 +1,94 @@
import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; import {
INodeProperties,
INodePropertyOptions
} from 'n8n-workflow';
export const resource = { export const resource = {
name: "Locale", name: 'Locale',
value: "locale", value: 'locale',
} as INodePropertyOptions; } as INodePropertyOptions;
export const operations = [ export const operations = [
{ {
displayName: "Operation", displayName: 'Operation',
name: "operation", name: 'operation',
type: "options", type: 'options',
displayOptions: { displayOptions: {
show: { show: {
resource: [resource.value], resource: [
resource.value,
],
}, },
}, },
options: [ options: [
{ {
name: "Get Locales", name: 'Get All',
value: "get_locales", value: 'getAll',
}, },
], ],
default: "get_locales", default: 'getAll',
description: "The operation to perform.", description: 'The operation to perform.',
}, },
] as INodeProperties[]; ] as INodeProperties[];
export const fields = [] as INodeProperties[]; export const fields = [
{
displayName: 'Environment ID',
name: 'environmentId',
type: 'string',
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'get',
'getAll',
],
},
},
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".'
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -1,28 +1,32 @@
import { INodeProperties } from "n8n-workflow"; import {
INodeProperties,
} from 'n8n-workflow';
export const resource = { export const resource = {
name: "Space", name: 'Space',
value: "space", value: 'space',
}; };
export const operations = [ export const operations = [
{ {
displayName: "Operation", displayName: 'Operation',
name: "operation", name: 'operation',
type: "options", type: 'options',
displayOptions: { displayOptions: {
show: { show: {
resource: [resource.value], resource: [
resource.value
],
}, },
}, },
options: [ options: [
{ {
name: "Get Space", name: 'Get',
value: "get_space", value: 'get',
}, },
], ],
default: "get_space", default: 'get',
description: "The operation to perform.", description: 'The operation to perform.',
}, },
] as INodeProperties[]; ] as INodeProperties[];

View file

@ -1,156 +0,0 @@
// @ts-nocheck
// Code from https://github.com/contentful/contentful-resolve-response/blob/master/index.js
import { cloneDeep } from 'lodash';
const UNRESOLVED_LINK = {}; // unique object to avoid polyfill bloat using Symbol()
/**
* isLink Function
* Checks if the object has sys.type "Link"
* @param object
*/
const isLink = (object: { sys: { type: string } }) =>
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<String>} 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;

View file

@ -43,7 +43,7 @@
"dist/credentials/ClockifyApi.credentials.js", "dist/credentials/ClockifyApi.credentials.js",
"dist/credentials/CockpitApi.credentials.js", "dist/credentials/CockpitApi.credentials.js",
"dist/credentials/CodaApi.credentials.js", "dist/credentials/CodaApi.credentials.js",
"dist/credentials/ContentfulDeliveryApi.credentials.js", "dist/credentials/ContentfulApi.credentials.js",
"dist/credentials/CopperApi.credentials.js", "dist/credentials/CopperApi.credentials.js",
"dist/credentials/CalendlyApi.credentials.js", "dist/credentials/CalendlyApi.credentials.js",
"dist/credentials/DisqusApi.credentials.js", "dist/credentials/DisqusApi.credentials.js",