From 8424c792dc924012391b52d5cdae12291f6378c7 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 8 Dec 2020 02:40:29 -0500 Subject: [PATCH] :sparkles: Add Ghost-Node (#1221) * :sparkles: Ghost Node * :zap: Minor improvements to Ghost-Node Co-authored-by: Jan Oberhauser --- .../credentials/GhostAdminApi.credentials.ts | 25 + .../GhostContentApi.credentials.ts | 25 + .../nodes/Ghost/GenericFunctions.ts | 109 ++ packages/nodes-base/nodes/Ghost/Ghost.node.ts | 352 +++++++ .../nodes-base/nodes/Ghost/PostDescription.ts | 931 ++++++++++++++++++ packages/nodes-base/nodes/Ghost/ghost.png | Bin 0 -> 1224 bytes packages/nodes-base/package.json | 3 + 7 files changed, 1445 insertions(+) create mode 100644 packages/nodes-base/credentials/GhostAdminApi.credentials.ts create mode 100644 packages/nodes-base/credentials/GhostContentApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Ghost/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Ghost/Ghost.node.ts create mode 100644 packages/nodes-base/nodes/Ghost/PostDescription.ts create mode 100644 packages/nodes-base/nodes/Ghost/ghost.png diff --git a/packages/nodes-base/credentials/GhostAdminApi.credentials.ts b/packages/nodes-base/credentials/GhostAdminApi.credentials.ts new file mode 100644 index 0000000000..db5c05c866 --- /dev/null +++ b/packages/nodes-base/credentials/GhostAdminApi.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GhostAdminApi implements ICredentialType { + name = 'ghostAdminApi'; + displayName = 'Ghost Admin API'; + documentationUrl = 'ghost'; + properties = [ + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'http://localhost:3001', + }, + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/GhostContentApi.credentials.ts b/packages/nodes-base/credentials/GhostContentApi.credentials.ts new file mode 100644 index 0000000000..9454f0c788 --- /dev/null +++ b/packages/nodes-base/credentials/GhostContentApi.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GhostContentApi implements ICredentialType { + name = 'ghostContentApi'; + displayName = 'Ghost Content API'; + documentationUrl = 'ghost'; + properties = [ + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'http://localhost:3001', + }, + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Ghost/GenericFunctions.ts b/packages/nodes-base/nodes/Ghost/GenericFunctions.ts new file mode 100644 index 0000000000..89664f576e --- /dev/null +++ b/packages/nodes-base/nodes/Ghost/GenericFunctions.ts @@ -0,0 +1,109 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import * as jwt from 'jsonwebtoken'; + +export async function ghostApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise { // tslint:disable-line:no-any + + const source = this.getNodeParameter('source', 0) as string; + + let credentials; + let version; + let token; + + if (source === 'contentApi') { + //https://ghost.org/faq/api-versioning/ + version = 'v3'; + credentials = this.getCredentials('ghostContentApi') as IDataObject; + query.key = credentials.apiKey as string; + } else { + version = 'v2'; + credentials = this.getCredentials('ghostAdminApi') as IDataObject; + // Create the token (including decoding secret) + const [id, secret] = (credentials.apiKey as string).split(':'); + + token = jwt.sign({}, Buffer.from(secret, 'hex'), { + keyid: id, + algorithm: 'HS256', + expiresIn: '5m', + audience: `/${version}/admin/`, + }); + } + + const options: OptionsWithUri = { + method, + qs: query, + uri: uri || `${credentials.url}/ghost/api/${version}${endpoint}`, + body, + json: true, + }; + + if (token) { + options.headers = { + Authorization: `Ghost ${token}`, + }; + } + + try { + return await this.helpers.request!(options); + + } catch (error) { + let errorMessages; + + if (error.response && error.response.body && error.response.body.errors) { + + if (Array.isArray(error.response.body.errors)) { + + const errors = error.response.body.errors; + + errorMessages = errors.map((e: IDataObject) => e.message); + } + + throw new Error(`Ghost error response [${error.statusCode}]: ${errorMessages?.join('|')}`); + } + + throw error; + } +} + +export async function ghostApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.limit = 20; + + let uri: string | undefined; + + do { + responseData = await ghostApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData.meta.pagination.next; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.meta.pagination.next !== null + ); + return returnData; +} + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} diff --git a/packages/nodes-base/nodes/Ghost/Ghost.node.ts b/packages/nodes-base/nodes/Ghost/Ghost.node.ts new file mode 100644 index 0000000000..ab2a3ea22c --- /dev/null +++ b/packages/nodes-base/nodes/Ghost/Ghost.node.ts @@ -0,0 +1,352 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + ghostApiRequest, + ghostApiRequestAllItems, + validateJSON, +} from './GenericFunctions'; + +import { + postFields, + postOperations, +} from './PostDescription'; + +import * as moment from 'moment-timezone'; + +export class Ghost implements INodeType { + description: INodeTypeDescription = { + displayName: 'Ghost', + name: 'ghost', + icon: 'file:ghost.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Ghost API.', + defaults: { + name: 'Ghost', + color: '#15212a', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'ghostAdminApi', + required: true, + displayOptions: { + show: { + source: [ + 'adminApi', + ], + }, + }, + }, + { + name: 'ghostContentApi', + required: true, + displayOptions: { + show: { + source: [ + 'contentApi', + ], + }, + }, + }, + ], + properties: [ + { + displayName: 'Source', + name: 'source', + type: 'options', + description: 'Pick where your data comes from, Content or Admin API', + options: [ + { + name: 'Admin API', + value: 'adminApi', + }, + { + name: 'Content API', + value: 'contentApi', + }, + ], + default: 'contentApi', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Post', + value: 'post', + }, + ], + default: 'post', + description: 'The resource to operate on.', + }, + ...postOperations, + ...postFields, + ], + }; + + + methods = { + loadOptions: { + // Get all the authors to display them to user so that he can + // select them easily + async getAuthors( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const users = await ghostApiRequestAllItems.call( + this, + 'users', + 'GET', + `/admin/users`, + ); + for (const user of users) { + returnData.push({ + name: user.name, + value: user.id, + }); + } + return returnData; + }, + // Get all the tags to display them to user so that he can + // select them easily + async getTags( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await ghostApiRequestAllItems.call( + this, + 'tags', + 'GET', + `/admin/tags`, + ); + for (const tag of tags) { + returnData.push({ + name: tag.name, + value: tag.id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const timezone = this.getTimezone(); + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + const source = this.getNodeParameter('source', 0) as string; + + if (source === 'contentApi') { + if (resource === 'post') { + if (operation === 'get') { + for (let i = 0; i < items.length; i++) { + const by = this.getNodeParameter('by', i) as string; + + const identifier = this.getNodeParameter('identifier', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + Object.assign(qs, options); + + let endpoint; + + if (by === 'slug') { + endpoint = `/content/posts/slug/${identifier}`; + } else { + endpoint = `/content/posts/${identifier}`; + } + responseData = await ghostApiRequest.call(this, 'GET', endpoint, {}, qs); + + returnData.push.apply(returnData, responseData.posts); + } + } + + if (operation === 'getAll') { + for (let i = 0; i < items.length; i++) { + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const options = this.getNodeParameter('options', i) as IDataObject; + + Object.assign(qs, options); + + if (returnAll) { + responseData = await ghostApiRequestAllItems.call(this, 'posts', 'GET', '/content/posts', {} ,qs); + } else { + qs.limit = this.getNodeParameter('limit', 0); + responseData = await ghostApiRequest.call(this, 'GET', '/content/posts', {}, qs); + responseData = responseData.posts; + } + + returnData.push.apply(returnData, responseData); + } + } + } + } + + if (source === 'adminApi') { + if (resource === 'post') { + if (operation === 'create') { + for (let i = 0; i < length; i++) { + const title = this.getNodeParameter('title', i) as string; + + const contentFormat = this.getNodeParameter('contentFormat', i) as string; + + const content = this.getNodeParameter('content', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const post: IDataObject = { + title, + }; + + if (contentFormat === 'html') { + post.html = content; + qs.source = 'html'; + } else { + const mobileDoc = validateJSON(content); + if (mobileDoc === undefined) { + throw new Error('Content must be a valid JSON'); + } + post.mobiledoc = content; + } + + delete post.content; + + Object.assign(post, additionalFields); + + if (post.published_at) { + post.published_at = moment.tz(post.published_at, timezone).utc().format(); + } + + if (post.status === 'scheduled' && post.published_at === undefined) { + throw new Error('Published at must be define when status is scheduled'); + } + + responseData = await ghostApiRequest.call(this, 'POST', '/admin/posts', { posts: [post] }, qs); + + returnData.push.apply(returnData, responseData.posts); + } + } + + if (operation === 'delete') { + for (let i = 0; i < length; i++) { + const postId = this.getNodeParameter('postId', i) as string; + + responseData = await ghostApiRequest.call(this, 'DELETE', `/admin/posts/${postId}`); + + returnData.push({ success: true }); + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + const by = this.getNodeParameter('by', i) as string; + + const identifier = this.getNodeParameter('identifier', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + Object.assign(qs, options); + + let endpoint; + + if (by === 'slug') { + endpoint = `/admin/posts/slug/${identifier}`; + } else { + endpoint = `/admin/posts/${identifier}`; + } + responseData = await ghostApiRequest.call(this, 'GET', endpoint, {}, qs); + + returnData.push.apply(returnData, responseData.posts); + } + } + + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const options = this.getNodeParameter('options', i) as IDataObject; + + Object.assign(qs, options); + + if (returnAll) { + responseData = await ghostApiRequestAllItems.call(this, 'posts', 'GET', '/admin/posts', {} ,qs); + } else { + qs.limit = this.getNodeParameter('limit', 0); + responseData = await ghostApiRequest.call(this, 'GET', '/admin/posts', {}, qs); + responseData = responseData.posts; + } + + returnData.push.apply(returnData, responseData); + } + } + + if (operation === 'update') { + for (let i = 0; i < length; i++) { + const postId = this.getNodeParameter('postId', i) as string; + + const contentFormat = this.getNodeParameter('contentFormat', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const post: IDataObject = {}; + + if (contentFormat === 'html') { + post.html = updateFields.content || ''; + qs.source = 'html'; + delete updateFields.content; + } else { + const mobileDoc = validateJSON(updateFields.contentJson as string || undefined); + if (mobileDoc === undefined) { + throw new Error('Content must be a valid JSON'); + } + post.mobiledoc = updateFields.contentJson; + delete updateFields.contentJson; + } + + Object.assign(post, updateFields); + + const { posts } = await ghostApiRequest.call(this, 'GET', `/admin/posts/${postId}`, {}, { fields: 'id, updated_at' }); + + if (post.published_at) { + post.published_at = moment.tz(post.published_at, timezone).utc().format(); + } + + if (post.status === 'scheduled' && post.published_at === undefined) { + throw new Error('Published at must be define when status is scheduled'); + } + + post.updated_at = posts[0].updated_at; + + responseData = await ghostApiRequest.call(this, 'PUT', `/admin/posts/${postId}`, { posts: [post] }, qs); + + returnData.push.apply(returnData, responseData.posts); + } + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Ghost/PostDescription.ts b/packages/nodes-base/nodes/Ghost/PostDescription.ts new file mode 100644 index 0000000000..c5dbd3bb4b --- /dev/null +++ b/packages/nodes-base/nodes/Ghost/PostDescription.ts @@ -0,0 +1,931 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const postOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + source: [ + 'contentApi', + ], + resource: [ + 'post', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a post', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all posts', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a post', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a post', + }, + { + name: 'Get', + value: 'get', + description: 'Get a post', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all posts', + }, + { + name: 'Update', + value: 'update', + description: 'Update a post', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const postFields = [ + /* -------------------------------------------------------------------------- */ + /* post:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + description: `Post's title.`, + }, + { + displayName: 'Content Format', + name: 'contentFormat', + type: 'options', + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Mobile Doc', + value: 'mobileDoc', + }, + ], + default: 'html', + description: `The format of the post.`, + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'create', + ], + contentFormat: [ + 'html', + ], + }, + }, + default: '', + description: 'The content of the post to create.', + }, + { + displayName: 'Content (JSON)', + name: 'content', + type: 'json', + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'create', + ], + contentFormat: [ + 'mobileDoc', + ], + }, + }, + + default: '', + description: 'Mobiledoc is the raw JSON format that Ghost uses to store post contents. Info', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Authors IDs', + name: 'authors', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getAuthors', + }, + default: [], + }, + { + displayName: 'Cannonical URL', + name: 'canonical_url', + type: 'string', + default: '', + }, + { + displayName: 'Code Injection Foot', + name: 'codeinjection_foot', + type: 'string', + default: '', + description: 'The Code Injection allows you inject a small snippet into your Ghost site', + }, + { + displayName: 'Code Injection Head', + name: 'codeinjection_head', + type: 'string', + default: '', + description: 'The Code Injection allows you inject a small snippet into your Ghost site', + }, + { + displayName: 'Featured', + name: 'featured', + type: 'boolean', + default: false, + }, + { + displayName: 'Meta Description', + name: 'meta_description', + type: 'string', + default: '', + }, + { + displayName: 'Meta Title', + name: 'meta_title', + type: 'string', + default: '', + }, + { + displayName: 'Open Graph Description', + name: 'og_description', + type: 'string', + default: '', + }, + { + displayName: 'Open Graph Title', + name: 'og_title', + type: 'string', + default: '', + }, + { + displayName: 'Open Graph Image', + name: 'og_image', + type: 'string', + default: '', + description: 'URL of the image', + + }, + { + displayName: 'Published At', + name: 'published_at', + type: 'dateTime', + default: '', + }, + { + displayName: 'Slug', + name: 'slug', + type: 'string', + default: '', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Draft', + value: 'draft', + }, + { + name: 'Published', + value: 'published', + }, + { + name: 'Scheduled', + value: 'scheduled', + }, + ], + default: 'draft', + }, + { + displayName: 'Tags IDs', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + }, + { + displayName: 'Twitter Description', + name: 'twitter_description', + type: 'string', + default: '', + }, + { + displayName: 'Twitter Image', + name: 'twitter_image', + type: 'string', + default: '', + description: 'URL of the image', + }, + { + displayName: 'Twitter Title', + name: 'twitter_title', + type: 'string', + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* post:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'The ID of the post to delete.', + }, + + /* -------------------------------------------------------------------------- */ + /* post:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'By', + name: 'by', + type: 'options', + default: '', + required: true, + options: [ + { + name: 'ID', + value: 'id', + }, + { + name: 'Slug', + value: 'slug', + }, + ], + displayOptions: { + show: { + source: [ + 'contentApi', + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Get the post either by slug or ID.', + }, + { + displayName: 'Identifier', + name: 'identifier', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + source: [ + 'contentApi', + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The ID or slug of the post to get.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Limit the fields returned in the response object. E.g. for posts fields=title,url.', + }, + { + displayName: 'Formats', + name: 'formats', + type: 'multiOptions', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Mobile Doc', + value: 'mobiledoc', + }, + ], + default: [ + 'mobiledoc', + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + source: [ + 'contentApi', + ], + resource: [ + 'post', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Limit the fields returned in the response object. E.g. for posts fields=title,url.', + }, + { + displayName: 'Formats', + name: 'formats', + type: 'multiOptions', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Plaintext', + value: 'plaintext', + }, + ], + default: [ + 'html', + ], + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* post:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + source: [ + 'contentApi', + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your user contacts.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + source: [ + 'adminApi', + 'contentApi', + ], + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + source: [ + 'contentApi', + ], + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Include', + name: 'include', + type: 'multiOptions', + options: [ + { + name: 'Authors', + value: 'authors', + }, + { + name: 'Tags', + value: 'tags', + }, + ], + default: [], + description: 'Tells the API to return additional data related to the resource you have requested', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Limit the fields returned in the response object. E.g. for posts fields=title,url.', + }, + { + displayName: 'Formats', + name: 'formats', + type: 'multiOptions', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Plaintext', + value: 'plaintext', + }, + ], + default: [ + 'html', + ], + description: `By default, only html is returned, however each post and page in Ghost has 2 available formats: html and plaintext.`, + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Include', + name: 'include', + type: 'multiOptions', + options: [ + { + name: 'Authors', + value: 'authors', + }, + { + name: 'Tags', + value: 'tags', + }, + ], + default: [], + description: 'Tells the API to return additional data related to the resource you have requested', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Limit the fields returned in the response object. E.g. for posts fields=title,url.', + }, + { + displayName: 'Formats', + name: 'formats', + type: 'multiOptions', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Mobile Doc', + value: 'mobiledoc', + }, + ], + default: [ + 'mobiledoc', + ], + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* post:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'The ID of the post to update.', + }, + { + displayName: 'Content Format', + name: 'contentFormat', + type: 'options', + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Mobile Doc', + value: 'mobileDoc', + }, + ], + default: 'html', + description: `The format of the post.`, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + source: [ + 'adminApi', + ], + resource: [ + 'post', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Authors IDs', + name: 'authors', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getAuthors', + }, + default: [], + }, + { + displayName: 'Cannonical URL', + name: 'canonical_url', + type: 'string', + default: '', + }, + { + displayName: 'Code Injection Foot', + name: 'codeinjection_foot', + type: 'string', + default: '', + }, + { + displayName: 'Code Injection Head', + name: 'codeinjection_head', + type: 'string', + default: '', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + displayOptions: { + show: { + '/contentFormat': [ + 'html', + ], + }, + }, + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Content (JSON)', + name: 'contentJson', + type: 'json', + displayOptions: { + show: { + '/contentFormat': [ + 'mobileDoc', + ], + }, + }, + default: '', + description: 'Mobiledoc is the raw JSON format that Ghost uses to store post contents. Info.', + }, + { + displayName: 'Featured', + name: 'featured', + type: 'boolean', + default: false, + }, + { + displayName: 'Meta Description', + name: 'meta_description', + type: 'string', + default: '', + }, + { + displayName: 'Meta Title', + name: 'meta_title', + type: 'string', + default: '', + }, + { + displayName: 'Open Graph Description', + name: 'og_description', + type: 'string', + default: '', + }, + { + displayName: 'Open Graph Title', + name: 'og_title', + type: 'string', + default: '', + }, + { + displayName: 'Open Graph Image', + name: 'og_image', + type: 'string', + default: '', + description: 'URL of the image', + }, + { + displayName: 'Published At', + name: 'published_at', + type: 'dateTime', + default: '', + }, + { + displayName: 'Slug', + name: 'slug', + type: 'string', + default: '', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Draft', + value: 'draft', + }, + { + name: 'Published', + value: 'published', + }, + { + name: 'Scheduled', + value: 'scheduled', + }, + ], + default: 'draft', + }, + { + displayName: 'Tags IDs', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: `Post's title`, + }, + { + displayName: 'Twitter Description', + name: 'twitter_description', + type: 'string', + default: '', + }, + { + displayName: 'Twitter Image', + name: 'twitter_image', + type: 'string', + default: '', + description: 'URL of the image', + }, + { + displayName: 'Twitter Title', + name: 'twitter_title', + type: 'string', + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Ghost/ghost.png b/packages/nodes-base/nodes/Ghost/ghost.png new file mode 100644 index 0000000000000000000000000000000000000000..6fdd457e466fa3039f39f59f9a2cd836d361c6ab GIT binary patch literal 1224 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bYNg$6c6wTaRqWk6}5nf!N?ZE5LEzi zMHMwg6ttltUuS^z+y6!U~$_FJ3OItkE*GkX6(B z|NsBr|Nnpg{jZ{9{Pp`!20qyjpT1PpHYjKszIgQp=!YxUZX7#tih*5n-@(HZr%YoI zR$yQd{PpMGfkQ`k@7 zj*3h2^bKMZQwj`?+|ZUU3UrocNswPK!{6U5-(L4!t5*MTZpX?}KW9@xZuZBIPTaq> zcV>NIUWTXCk@lw>7e>k(hc;F*Fff~Yx;Tb-9A7%UJ?WN#K%4639JiYS9t{(8PaaXt zOHL|NySZ4|^YZulzIhyGnJSlW{ghPiKR@Z|<)3_KMY$9M9KEkEUYVt|O@vchLi^!~ zAe}&8*VmbAAIwqhdVglsNu|UX5tn6;bdR}qd77T~pR&-`)U8n|f-&)hXt5YiPJv*Z z$uhIpnsd(jsw2yK|`_)x{^CvR8tff;#ECc1TS1c#qn#v#0D zp^0!r@s)SmWCN3;Hdq?5&RQDiyCe1TrTZsTj#+M>rg^8t_tEUwzi(cg_HhrNJn z?py7>w)#zB`4WEps=2|8$4nGI<}9u^{$q9d!>;^0r3e4Lk`{We$7wtJSf}0f6B{IE z2p<-y(o>8S*~k`B3=Lp+BkxeBXvv;4igGcVrv{*)ukOE@y3{~Jow|MPx)uzauX zI$o7;yVuXR+WdBfx?Q@!KY>3R6HlxAOsF~ZNGa!>oTx2J;Jr^(AxByLk{Wk>EtsSl z!Wi*x-eLF27k0a6RPRb}(u(6we*f~%lkX3hqAs-_Je84qaMGS@=jXHA-rJZZEQnEg@p_q%(}y)o2W>tnZISz;dy~OJIOE2F3ejHuYQ?GUn*z_={i=-A@WF0%3yL+yq=%vd>viUIY&(2nin3`VV*JDDEX-S z{ux*AsomLn;H1@ZV^_BDBmWzP#QgW`8*ynDx>e3p+}jlWJfLlQaZ+V-(O091Q{4^} z6i(O}6!Lk+nhDoDLPcJMq`rGJ?``Mw#WS1AeUJYyGwS`iHsz)+`{SctNx8+5ch5b$ cH@&%@;f=?4Hl68@K*a%rr>mdKI;Vst02