From 48362f50ef78f27e2e10e32463fad6d4cab51a6b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 28 Jan 2021 13:00:47 -0500 Subject: [PATCH] :sparkles: Add Discourse Node (#1348) * :sparkles: Discourse Node * :zap: Add missing credential file * :zap: Improvements * :zap: Improvements * :zap: Minor improvements on Discourse Node Co-authored-by: Jan Oberhauser --- .../credentials/DiscourseApi.credentials.ts | 33 ++ .../nodes/Discourse/CategoryDescription.ts | 216 ++++++++ .../nodes/Discourse/Discourse.node.ts | 500 ++++++++++++++++++ .../nodes/Discourse/GenericFunctions.ts | 64 +++ .../nodes/Discourse/GroupDescription.ts | 153 ++++++ .../nodes/Discourse/PostDescription.ts | 270 ++++++++++ .../nodes/Discourse/SearchDescription.ts | 69 +++ .../nodes/Discourse/UserDescription.ts | 308 +++++++++++ .../nodes/Discourse/UserGroupDescription.ts | 116 ++++ .../nodes-base/nodes/Discourse/discourse.svg | 1 + packages/nodes-base/package.json | 2 + 11 files changed, 1732 insertions(+) create mode 100644 packages/nodes-base/credentials/DiscourseApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Discourse/CategoryDescription.ts create mode 100644 packages/nodes-base/nodes/Discourse/Discourse.node.ts create mode 100644 packages/nodes-base/nodes/Discourse/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Discourse/GroupDescription.ts create mode 100644 packages/nodes-base/nodes/Discourse/PostDescription.ts create mode 100644 packages/nodes-base/nodes/Discourse/SearchDescription.ts create mode 100644 packages/nodes-base/nodes/Discourse/UserDescription.ts create mode 100644 packages/nodes-base/nodes/Discourse/UserGroupDescription.ts create mode 100644 packages/nodes-base/nodes/Discourse/discourse.svg diff --git a/packages/nodes-base/credentials/DiscourseApi.credentials.ts b/packages/nodes-base/credentials/DiscourseApi.credentials.ts new file mode 100644 index 0000000000..98f38911e2 --- /dev/null +++ b/packages/nodes-base/credentials/DiscourseApi.credentials.ts @@ -0,0 +1,33 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class DiscourseApi implements ICredentialType { + name = 'discourseApi'; + displayName = 'Discourse API'; + documentationUrl = 'discourse'; + properties = [ + { + displayName: 'URL', + name: 'url', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Key', + name: 'apiKey', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Username', + name: 'username', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Discourse/CategoryDescription.ts b/packages/nodes-base/nodes/Discourse/CategoryDescription.ts new file mode 100644 index 0000000000..71c8de21e6 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/CategoryDescription.ts @@ -0,0 +1,216 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const categoryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a category', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all categories', + }, + { + name: 'Update', + value: 'update', + description: 'Update a category', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const categoryFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* category:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Name of the category.', + }, + { + displayName: 'Color', + name: 'color', + type: 'color', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'create', + ], + }, + }, + default: '0000FF', + description: 'Color of the category.', + }, + { + displayName: 'Text Color', + name: 'textColor', + type: 'color', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'create', + ], + }, + }, + default: '0000FF', + description: 'Text color of the category.', + }, + + /* -------------------------------------------------------------------------- */ + /* category:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + + /* -------------------------------------------------------------------------- */ + /* category:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Category ID', + name: 'categoryId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'ID of the category.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'New name of the category.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Color', + name: 'color', + type: 'color', + default: '0000FF', + description: 'Color of the category', + }, + { + displayName: 'Text Color', + name: 'textColor', + type: 'color', + default: '0000FF', + description: 'Text color of the category', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Discourse/Discourse.node.ts b/packages/nodes-base/nodes/Discourse/Discourse.node.ts new file mode 100644 index 0000000000..78f68374f5 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/Discourse.node.ts @@ -0,0 +1,500 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + discourseApiRequest, +} from './GenericFunctions'; + +import { + postFields, + postOperations, +} from './PostDescription'; + +import { + categoryFields, + categoryOperations, +} from './CategoryDescription'; + +import { + groupFields, + groupOperations, +} from './GroupDescription'; + +// import { +// searchFields, +// searchOperations, +// } from './SearchDescription'; + +import { + userFields, + userOperations, +} from './UserDescription'; + +import { + userGroupFields, + userGroupOperations, +} from './UserGroupDescription'; + +//import * as moment from 'moment'; + +export class Discourse implements INodeType { + description: INodeTypeDescription = { + displayName: 'Discourse', + name: 'discourse', + icon: 'file:discourse.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Discourse API.', + defaults: { + name: 'Discourse', + color: '#000000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'discourseApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Category', + value: 'category', + }, + { + name: 'Group', + value: 'group', + }, + { + name: 'Post', + value: 'post', + }, + // { + // name: 'Search', + // value: 'search', + // }, + { + name: 'User', + value: 'user', + }, + { + name: 'User Group', + value: 'userGroup', + }, + ], + default: 'post', + description: 'The resource to operate on.', + }, + ...categoryOperations, + ...categoryFields, + ...groupOperations, + ...groupFields, + ...postOperations, + ...postFields, + // ...searchOperations, + // ...searchFields, + ...userOperations, + ...userFields, + ...userGroupOperations, + ...userGroupFields, + ], + }; + + methods = { + loadOptions: { + // Get all the calendars to display them to user so that he can + // select them easily + async getCategories( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const { category_list } = await discourseApiRequest.call( + this, + 'GET', + `/categories.json`, + ); + for (const category of category_list.categories) { + returnData.push({ + name: category.name, + value: category.id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'category') { + //https://docs.discourse.org/#tag/Categories/paths/~1categories.json/post + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const color = this.getNodeParameter('color', i) as string; + const textColor = this.getNodeParameter('textColor', i) as string; + + const body: IDataObject = { + name, + color, + text_color: textColor, + }; + + responseData = await discourseApiRequest.call( + this, + 'POST', + `/categories.json`, + body, + ); + + responseData = responseData.category; + } + //https://docs.discourse.org/#tag/Categories/paths/~1categories.json/get + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/categories.json`, + {}, + qs, + ); + + responseData = responseData.category_list.categories; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + //https://docs.discourse.org/#tag/Categories/paths/~1categories~1{id}/put + if (operation === 'update') { + const categoryId = this.getNodeParameter('categoryId', i) as string; + + const name = this.getNodeParameter('name', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = { + name, + }; + + Object.assign(body, updateFields); + + responseData = await discourseApiRequest.call( + this, + 'PUT', + `/categories/${categoryId}.json`, + body, + ); + + responseData = responseData.category; + } + } + if (resource === 'group') { + //https://docs.discourse.org/#tag/Posts/paths/~1posts.json/post + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + + const body: IDataObject = { + name, + }; + + responseData = await discourseApiRequest.call( + this, + 'POST', + `/admin/groups.json`, + { group: body }, + ); + + responseData = responseData.basic_group; + } + //https://docs.discourse.org/#tag/Groups/paths/~1groups~1{name}.json/get + if (operation === 'get') { + const name = this.getNodeParameter('name', i) as string; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/groups/${name}`, + {}, + qs, + ); + + responseData = responseData.group; + + } + //https://docs.discourse.org/#tag/Groups/paths/~1groups.json/get + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/groups.json`, + {}, + qs, + ); + + responseData = responseData.groups; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + //https://docs.discourse.org/#tag/Posts/paths/~1posts~1{id}.json/put + if (operation === 'update') { + const groupId = this.getNodeParameter('groupId', i) as string; + + const name = this.getNodeParameter('name', i) as string; + + const body: IDataObject = { + name, + }; + + responseData = await discourseApiRequest.call( + this, + 'PUT', + `/groups/${groupId}.json`, + { group: body }, + ); + } + } + if (resource === 'post') { + //https://docs.discourse.org/#tag/Posts/paths/~1posts.json/post + if (operation === 'create') { + const content = this.getNodeParameter('content', i) as string; + const title = this.getNodeParameter('title', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + title, + raw: content, + }; + + Object.assign(body, additionalFields); + + responseData = await discourseApiRequest.call( + this, + 'POST', + `/posts.json`, + body, + ); + } + //https://docs.discourse.org/#tag/Posts/paths/~1posts~1{id}.json/get + if (operation === 'get') { + const postId = this.getNodeParameter('postId', i) as string; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/posts/${postId}`, + {}, + qs, + ); + } + //https://docs.discourse.org/#tag/Posts/paths/~1posts.json/get + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/posts.json`, + {}, + qs, + ); + + responseData = responseData.latest_posts; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + //https://docs.discourse.org/#tag/Posts/paths/~1posts~1{id}.json/put + if (operation === 'update') { + const postId = this.getNodeParameter('postId', i) as string; + + const content = this.getNodeParameter('content', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = { + raw: content, + }; + + Object.assign(body, updateFields); + + responseData = await discourseApiRequest.call( + this, + 'PUT', + `/posts/${postId}.json`, + body, + ); + + responseData = responseData.post; + } + } + // TODO figure how to paginate the results + // if (resource === 'search') { + // //https://docs.discourse.org/#tag/Search/paths/~1search~1query/get + // if (operation === 'query') { + // qs.term = this.getNodeParameter('term', i) as string; + + // const simple = this.getNodeParameter('simple', i) as boolean; + + // const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + // Object.assign(qs, updateFields); + + // qs.page = 1; + + // responseData = await discourseApiRequest.call( + // this, + // 'GET', + // `/search/query`, + // {}, + // qs, + // ); + + // if (simple === true) { + // const response = []; + // for (const key of Object.keys(responseData)) { + // console.log(key) + // for (const data of responseData[key]) { + // response.push(Object.assign(data, { __type: key })); + // } + // } + // responseData = response; + // } + // } + // } + if (resource === 'user') { + //https://docs.discourse.org/#tag/Users/paths/~1users/post + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const email = this.getNodeParameter('email', i) as string; + const password = this.getNodeParameter('password', i) as string; + const username = this.getNodeParameter('username', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + name, + password, + email, + username, + }; + + Object.assign(body, additionalFields); + + responseData = await discourseApiRequest.call( + this, + 'POST', + `/users.json`, + body, + ); + } + //https://docs.discourse.org/#tag/Users/paths/~1users~1{username}.json/get + if (operation === 'get') { + const by = this.getNodeParameter('by', i) as string; + let endpoint = ''; + if (by === 'username') { + const username = this.getNodeParameter('username', i) as string; + endpoint = `/users/${username}`; + } else if (by === 'externalId') { + const externalId = this.getNodeParameter('externalId', i) as string; + endpoint = `/u/by-external/${externalId}.json`; + } + + responseData = await discourseApiRequest.call( + this, + 'GET', + endpoint, + ); + } + //https://docs.discourse.org/#tag/Users/paths/~1admin~1users~1{id}.json/delete + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const flag = this.getNodeParameter('flag', i) as boolean; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/admin/users/list/${flag}.json`, + {}, + qs, + ); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + } + if (resource === 'userGroup') { + //https://docs.discourse.org/#tag/Groups/paths/~1groups~1{group_id}~1members.json/put + if (operation === 'add') { + const usernames = this.getNodeParameter('usernames', i) as string; + const groupId = this.getNodeParameter('groupId', i) as string; + const body: IDataObject = { + usernames, + }; + + responseData = await discourseApiRequest.call( + this, + 'PUT', + `/groups/${groupId}/members.json`, + body, + ); + } + //https://docs.discourse.org/#tag/Groups/paths/~1groups~1{group_id}~1members.json/delete + if (operation === 'remove') { + const usernames = this.getNodeParameter('usernames', i) as string; + const groupId = this.getNodeParameter('groupId', i) as string; + const body: IDataObject = { + usernames, + }; + + responseData = await discourseApiRequest.call( + this, + 'DELETE', + `/groups/${groupId}/members.json`, + body, + ); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Discourse/GenericFunctions.ts b/packages/nodes-base/nodes/Discourse/GenericFunctions.ts new file mode 100644 index 0000000000..0a8057bd08 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/GenericFunctions.ts @@ -0,0 +1,64 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function discourseApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('discourseApi') as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'Api-Key': credentials.apiKey, + 'Api-Username': credentials.username, + }, + method, + body, + qs, + uri: `${credentials.url}${path}`, + json: true, + }; + + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.request.call(this, options); + } catch (error) { + if (error.response && error.response.body && error.response.body.errors) { + + const errors = error.response.body.errors; + // Try to return the error prettier + throw new Error( + `Discourse error response [${error.statusCode}]: ${errors.join('|')}`, + ); + } + throw error; + } +} + +export async function discourseApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + do { + responseData = await discourseApiRequest.call(this, method, endpoint, body, query); + returnData.push.apply(returnData, responseData); + query.page++; + } while ( + responseData.length !== 0 + ); + return returnData; +} diff --git a/packages/nodes-base/nodes/Discourse/GroupDescription.ts b/packages/nodes-base/nodes/Discourse/GroupDescription.ts new file mode 100644 index 0000000000..45448d89c0 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/GroupDescription.ts @@ -0,0 +1,153 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const groupOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'group', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a group', + }, + { + name: 'Get', + value: 'get', + description: 'Get a group', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all groups', + }, + { + name: 'Update', + value: 'update', + description: 'Update a group', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const groupFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* group:create & get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'group', + ], + operation: [ + 'get', + 'create', + ], + }, + }, + default: '', + description: 'Name of the group.', + }, + + /* -------------------------------------------------------------------------- */ + /* group:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'group', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'group', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + + /* -------------------------------------------------------------------------- */ + /* group:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Group ID', + name: 'groupId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'group', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'ID of the group to update.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'group', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'New name of the group.', + }, +]; diff --git a/packages/nodes-base/nodes/Discourse/PostDescription.ts b/packages/nodes-base/nodes/Discourse/PostDescription.ts new file mode 100644 index 0000000000..858ce64ac6 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/PostDescription.ts @@ -0,0 +1,270 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const postOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create 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: 'create', + }, +] as INodeProperties[]; + +export const postFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* post:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Title of the post.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Content of the post.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'post', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Category ID', + name: 'category', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCategories', + }, + default: '', + description: 'ID of the category', + }, + { + displayName: 'Reply To Post Number', + name: 'reply_to_post_number', + type: 'string', + default: '', + description: 'The number of the post to reply to', + }, + { + displayName: 'Topic ID', + name: 'topic_id', + type: 'string', + default: '', + description: 'ID of the topic', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* post:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'ID of the post.', + }, + + /* -------------------------------------------------------------------------- */ + /* post:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + /* -------------------------------------------------------------------------- */ + /* post:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'ID of the post.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Content of the post. HTML is supported.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Edit Reason', + name: 'edit_reason', + type: 'string', + default: '', + }, + { + displayName: 'Cooked', + name: 'cooked', + type: 'boolean', + default: false, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Discourse/SearchDescription.ts b/packages/nodes-base/nodes/Discourse/SearchDescription.ts new file mode 100644 index 0000000000..f9bc7b9756 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/SearchDescription.ts @@ -0,0 +1,69 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const searchOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'search', + ], + }, + }, + options: [ + { + name: 'Query', + value: 'query', + description: 'Search for something', + }, + ], + default: 'query', + }, +] as INodeProperties[]; + +export const searchFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* search:query */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Term', + name: 'term', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'search', + ], + operation: [ + 'query', + ], + }, + }, + default: '', + description: 'Term to search for.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'search', + ], + operation: [ + 'query', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, +]; diff --git a/packages/nodes-base/nodes/Discourse/UserDescription.ts b/packages/nodes-base/nodes/Discourse/UserDescription.ts new file mode 100644 index 0000000000..3c32eef7a3 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/UserDescription.ts @@ -0,0 +1,308 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a user', + }, + { + name: 'Get', + value: 'get', + description: 'Get a user', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all users', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const userFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* user:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Name of the user to create.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Email of the user to create.', + }, + { + displayName: 'Username', + name: 'username', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The username of the user to create.`, + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + typeOptions: { + password: true, + }, + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The password of the user to create.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: false, + }, + { + displayName: 'Approved', + name: 'approved', + type: 'boolean', + default: false, + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* user:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'By', + name: 'by', + type: 'options', + options: [ + { + name: 'Username', + value: 'username', + }, + { + name: 'SSO External ID', + value: 'externalId', + }, + ], + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + default: 'username', + description: 'What to search by.', + }, + { + displayName: 'Username', + name: 'username', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + by: [ + 'username', + ], + }, + }, + default: '', + description: `The username of the user to return.`, + }, + { + displayName: 'SSO External ID', + name: 'externalId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + by: [ + 'externalId', + ], + }, + }, + default: '', + description: `Discourse SSO external ID.`, + }, + + /* -------------------------------------------------------------------------- */ + /* user:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Flag', + name: 'flag', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Blocked', + value: 'blocked', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Staff', + value: 'staff', + }, + { + name: 'Suspect', + value: 'suspect', + }, + ], + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + default: '', + description: `User flags to search for.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, +]; diff --git a/packages/nodes-base/nodes/Discourse/UserGroupDescription.ts b/packages/nodes-base/nodes/Discourse/UserGroupDescription.ts new file mode 100644 index 0000000000..cdd4fd0a77 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/UserGroupDescription.ts @@ -0,0 +1,116 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userGroupOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'userGroup', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Create a user to group', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove user from group', + }, + ], + default: 'add', + }, +] as INodeProperties[]; + +export const userGroupFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* userGroup:add */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Usernames', + name: 'usernames', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'userGroup', + ], + operation: [ + 'add', + ], + }, + }, + default: '', + description: 'Usernames to add to group. Multiples can be defined separated by comma', + }, + { + displayName: 'Group ID', + name: 'groupId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'userGroup', + ], + operation: [ + 'add', + ], + }, + }, + default: '', + description: 'ID of the group.', + }, + + /* -------------------------------------------------------------------------- */ + /* userGroup:remove */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Usernames', + name: 'usernames', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'userGroup', + ], + operation: [ + 'remove', + ], + }, + }, + default: '', + description: 'Usernames to remove from group. Multiples can be defined separated by comma.', + }, + { + displayName: 'Group ID', + name: 'groupId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'userGroup', + ], + operation: [ + 'remove', + ], + }, + }, + default: '', + description: 'ID of the group to remove.', + }, + +]; diff --git a/packages/nodes-base/nodes/Discourse/discourse.svg b/packages/nodes-base/nodes/Discourse/discourse.svg new file mode 100644 index 0000000000..73e7d63ece --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/discourse.svg @@ -0,0 +1 @@ +Discourse_logo \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 8c774a701e..1ace196314 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -61,6 +61,7 @@ "dist/credentials/CustomerIoApi.credentials.js", "dist/credentials/S3.credentials.js", "dist/credentials/CrateDb.credentials.js", + "dist/credentials/DiscourseApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", "dist/credentials/DriftApi.credentials.js", "dist/credentials/DriftOAuth2Api.credentials.js", @@ -296,6 +297,7 @@ "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", "dist/nodes/DateTime.node.js", "dist/nodes/Discord/Discord.node.js", + "dist/nodes/Discourse/Discourse.node.js", "dist/nodes/Disqus/Disqus.node.js", "dist/nodes/Drift/Drift.node.js", "dist/nodes/Dropbox/Dropbox.node.js",