From 81ef9c6801892ed7c0bed6fd463467c75d38cbef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 12 Jun 2020 17:23:36 -0300 Subject: [PATCH] :sparkles: Hacker News node --- .../nodes/HackerNews/GenericFunctions.ts | 69 ++++ .../nodes/HackerNews/HackerNews.node.ts | 357 ++++++++++++++++++ .../nodes/HackerNews/hackernews.png | Bin 0 -> 1952 bytes packages/nodes-base/package.json | 1 + 4 files changed, 427 insertions(+) create mode 100644 packages/nodes-base/nodes/HackerNews/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/HackerNews/HackerNews.node.ts create mode 100644 packages/nodes-base/nodes/HackerNews/hackernews.png diff --git a/packages/nodes-base/nodes/HackerNews/GenericFunctions.ts b/packages/nodes-base/nodes/HackerNews/GenericFunctions.ts new file mode 100644 index 0000000000..56f6d8aef6 --- /dev/null +++ b/packages/nodes-base/nodes/HackerNews/GenericFunctions.ts @@ -0,0 +1,69 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, ILoadOptionsFunctions, +} from 'n8n-workflow'; + +import { + OptionsWithUri +} from 'request'; + + +/** + * Make an API request to HackerNews + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} endpoint + * @param {IDataObject} qs + * @returns {Promise} + */ +export async function hackerNewsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, qs: IDataObject): Promise { // tslint:disable-line:no-any + const options: OptionsWithUri = { + method: method, + qs, + uri: `http://hn.algolia.com/api/v1/${endpoint}`, + json: true, + }; + + return await this.helpers.request!(options); +} + + +/** + * Make an API request to HackerNews + * and return all results + * + * @export + * @param {(IHookFunctions | IExecuteFunctions)} this + * @param {string} method + * @param {string} endpoint + * @param {IDataObject} qs + * @returns {Promise} + */ +export async function hackerNewsApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, qs: IDataObject): Promise { // tslint:disable-line:no-any + + qs.hitsPerPage = 100; + + const returnData: IDataObject[] = []; + + let responseData; + let itemsReceived = 0; + + do { + responseData = await hackerNewsApiRequest.call(this, method, endpoint, qs); + returnData.push.apply(returnData, responseData.hits); + + if (returnData !== undefined) { + itemsReceived += returnData.length; + } + + } while ( + responseData.nbHits > itemsReceived + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/HackerNews/HackerNews.node.ts b/packages/nodes-base/nodes/HackerNews/HackerNews.node.ts new file mode 100644 index 0000000000..91edf0679d --- /dev/null +++ b/packages/nodes-base/nodes/HackerNews/HackerNews.node.ts @@ -0,0 +1,357 @@ +import { + IExecuteFunctions +} from 'n8n-core'; + +import { + INodeExecutionData, + INodeType, + INodeTypeDescription, + IDataObject, +} from 'n8n-workflow'; + +import { + hackerNewsApiRequest, + hackerNewsApiRequestAllItems +} from './GenericFunctions'; + +export class HackerNews implements INodeType { + description: INodeTypeDescription = { + displayName: 'Hacker News', + name: 'hackerNews', + icon: 'file:hackernews.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Hacker News API', + defaults: { + name: 'Hacker News', + color: '#ff6600', + }, + inputs: ['main'], + outputs: ['main'], + properties: [ + // ---------------------------------- + // Resources + // ---------------------------------- + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Article', + value: 'article' + }, + { + name: 'User', + value: 'user' + } + ], + default: 'article', + description: 'Resource to consume.', + }, + // ---------------------------------- + // Operations + // ---------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'article' + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a Hacker News article', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all Hacker News articles', + } + ], + default: 'get', + description: 'Operation to perform.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user' + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a Hacker News user', + } + ], + default: 'get', + description: 'Operation to perform.', + }, + // ---------------------------------- + // Fields + // ---------------------------------- + { + displayName: 'Article ID', + name: 'articleId', + type: 'string', + required: true, + default: '', + description: 'The ID of the Hacker News article to be returned', + displayOptions: { + show: { + resource: [ + 'article' + ], + operation: [ + 'get' + ], + }, + }, + }, + { + displayName: 'Username', + name: 'username', + type: 'string', + required: true, + default: '', + description: 'The Hacker News user to be returned', + displayOptions: { + show: { + resource: [ + 'user' + ], + operation: [ + 'get' + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results for the query or only up to a limit.', + displayOptions: { + show: { + resource: [ + 'article' + ], + operation: [ + 'getAll' + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'Limit of Hacker News articles to be returned for the query.', + displayOptions: { + show: { + resource: [ + 'article' + ], + operation: [ + 'getAll' + ], + returnAll: [ + false + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'article' + ], + operation: [ + 'get' + ], + }, + }, + options: [ + { + displayName: 'Include comments', + name: 'includeComments', + type: 'boolean', + default: false, + description: 'Whether to include all the comments in a Hacker News article.' + }, + ] + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'article' + ], + operation: [ + 'getAll' + ], + }, + }, + options: [ + { + displayName: 'Keyword', + name: 'keyword', + type: 'string', + default: '', + description: 'The keyword for filtering the results of the query.', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + options: [ + { + name: 'Story', + value: 'story', + description: 'Returns query results filtered by story tag', + }, + { + name: 'Comment', + value: 'comment', + description: 'Returns query results filtered by comment tag', + }, + { + name: 'Poll', + value: 'poll', + description: 'Returns query results filtered by poll tag', + }, + { + name: 'Show HN', + value: 'show_hn', // snake case per HN tags + description: 'Returns query results filtered by Show HN tag', + }, + { + name: 'Ask HN', + value: 'ask_hn', // snake case per HN tags + description: 'Returns query results filtered by Ask HN tag', + }, + { + name: 'Front Page', + value: 'front_page', // snake case per HN tags + description: 'Returns query results filtered by Front Page tag', + } + ], + default: '', + description: 'Tags for filtering the results of the query.', + } + ] + } + ] + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + let returnAll = false; + + for (let i = 0; i < items.length; i++) { + + let qs: IDataObject = {}; + let endpoint = ''; + let includeComments = false; + + if (resource === 'article') { + + if (operation === 'get') { + + endpoint = `items/${this.getNodeParameter('articleId', i)}`; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + includeComments = additionalFields.includeComments as boolean; + + } else if (operation === 'getAll') { + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const keyword = additionalFields.keyword as string; + const tags = additionalFields.tags as string[]; + + qs = { + query: keyword, + tags: tags ? tags.join() : '', + }; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (!returnAll) { + qs.hitsPerPage = this.getNodeParameter('limit', i) as number; + } + + endpoint = 'search?'; + + } else { + throw new Error(`The operation '${operation}' is unknown!`); + } + + } else if (resource === 'user') { + + if (operation === 'get') { + endpoint = `users/${this.getNodeParameter('username', i)}`; + + } else { + throw new Error(`The operation '${operation}' is unknown!`); + } + + } else { + throw new Error(`The resource '${resource}' is unknown!`); + } + + + let responseData; + if (returnAll === true) { + responseData = await hackerNewsApiRequestAllItems.call(this, 'GET', endpoint, qs); + } else { + responseData = await hackerNewsApiRequest.call(this, 'GET', endpoint, qs); + if (resource === 'article' && operation === 'getAll') + responseData = responseData.hits; + } + + if (resource === 'article' && operation === 'get' && !includeComments) { + delete responseData.children; + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + + } + + return [this.helpers.returnJsonArray(returnData)]; + + } +} diff --git a/packages/nodes-base/nodes/HackerNews/hackernews.png b/packages/nodes-base/nodes/HackerNews/hackernews.png new file mode 100644 index 0000000000000000000000000000000000000000..67ba3047ff3d07ec99bd35a2124161d8779cbb0f GIT binary patch literal 1952 zcmcgrX;f3m622?~23dy1u;_@03>aYof-0>ydUrHtJ8JrRQ30LU0tVJ;N`hp zjbuQ=Fied>clIXYmPJ=lSgaVEI}$-L*ooEH787{3! zg&cVpV~#{a;AIicRe@2)aJ(DFt^?Jl(A+dU>j3VCAp~HI84wMEKd!(-%7||WM_$1= z3amZ_XM`wa1H8e4U_nZxvF4DFs?WqZ3V5g%=zR=DgD}<%apXbXUi3{&bctjLR3@TN z?-7n*Wdh=2Pb;rXgd;Y#zT~Fz8)T7 zfJf-#T;;xwP)*{!8uPt9cVQT5y?(8HOKH^CvOGD|uhGUqj_M*#S`B(IaqB5VsbWt8tBB`rT-YVBsVwi$} z;q2r?e{pNC_~4+I=8hY;6wSO&)6CsFmld@I47{pJwu+RbKWO-m$<{Pf@p+w+fw?YF zauLjf&%^yedbyLVrK-J##%Wncd~^&$(x{V9o?PKRZ)`E`uqx`ayD+P@(XvNJdnINW zpRwtOoDVAArp+s6G%~Jfy=Jtoa505La1&7L+tSNBLDE3f& zwM3`q{8X~5%Z1!7lb-du2JxJWLL0aKBWn$!Y=gcl5URPxm-uN0zMi{o^=7%HS%pb` z$E1_J;fhPl$cFR>A0iqb8l8LDXZq4@c>KYP4p~p5KxC!;N;oz?o>O*RaaNbI`_1dS z=O;RD)~lCxtLx9Z8Lq|HC^{9 zGhd$mI)CsXpYcnObwy#u3Ja3l^*gsFcM5fNYUS??`hF*1<@u5euFsV*D-_#n_D5&B zPPXrqkG}n;@DIz?2F+H*0}~DWT*q~y=Q53m1Cg5@Gq0-ejy&U3&WIJnlnqY?NMtvJ zVD4G`bnii4aHmy31^1SnzmWl1J#5#e@6+OzneQCgN;Z6+^5WZACY7(J6m!DK{L7iF zg~RU-t-4N;~d@2^W0Ew)D=QXI8U*FFo+A;*OKywy{=d)eW5C+m`RH^buHRnz7< z{VmOHP0pHCHF@``yEt#1xPvJV^b(?GU6OuP3^LiB9@M-I1I9N;$vTWUEL^^HfjPQCLpp_Pt7HN$|eaf^rL5r*`^NR_c-PGO7iaX6W?@Of*r zyV*3(wrK zi)DVfrTcGd1~sKsnMRI|^AmnI8medBr)p(C6^aWQPPlhI0ycpjS_;*TB#rL_i?*CR z@|(UM{nQ5~hbYqDAHS(`XM3{UI@K(rmi{fOPM%-s-dU?})^u#xySZ;!%;3NFlndIi z>ZAfSuc4DfDB0URSDmzM~RZ_vItLaLJ`Z@la^b^oXmF+%T^AtfbkX&m_ykIvVH7is+u!`pE zN86W2+t)*jHceS@T>2?QXW+N>eG~5;NbVm^b`3YLBL1m9-{luR?HG^8KFY^vVe9}t zn`IOk!aroRox$|lo2X7EKx-F4EMm((=-&v;nrcn8pl-CVqWW0b*i-H7tv67pRC_9w h;ugB+zX-fztPoDr|0ej7TSNo_W4L%amu~Y<`UkDC^S=N9 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 37b9adecf2..c27f5fa98b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -190,6 +190,7 @@ "dist/nodes/Google/Sheet/GoogleSheets.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js", + "dist/nodes/HackerNews/HackerNews.node.js", "dist/nodes/Harvest/Harvest.node.js", "dist/nodes/HelpScout/HelpScout.node.js", "dist/nodes/HelpScout/HelpScoutTrigger.node.js",