diff --git a/packages/nodes-base/credentials/MarketstackApi.credentials.ts b/packages/nodes-base/credentials/MarketstackApi.credentials.ts new file mode 100644 index 0000000000..5c5a33c832 --- /dev/null +++ b/packages/nodes-base/credentials/MarketstackApi.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MarketstackApi implements ICredentialType { + name = 'marketstackApi'; + displayName = 'Marketstack API'; + documentationUrl = 'marketstack'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Use HTTPS', + name: 'useHttps', + type: 'boolean' as NodePropertyTypes, + default: false, + description: 'Use HTTPS (paid plans only).', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Marketstack/GenericFunctions.ts b/packages/nodes-base/nodes/Marketstack/GenericFunctions.ts new file mode 100644 index 0000000000..3a20c1ca26 --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/GenericFunctions.ts @@ -0,0 +1,96 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +export async function marketstackApiRequest( + this: IExecuteFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const credentials = this.getCredentials('marketstackApi') as IDataObject; + const protocol = credentials.useHttps ? 'https' : 'http'; // Free API does not support HTTPS + + const options: OptionsWithUri = { + method, + uri: `${protocol}://api.marketstack.com/v1${endpoint}`, + qs: { + access_key: credentials.apiKey, + ...qs, + }, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + try { + return await this.helpers.request(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +export async function marketstackApiRequestAllItems( + this: IExecuteFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean; + const limit = this.getNodeParameter('limit', 0, 0) as number; + + let responseData; + const returnData: IDataObject[] = []; + + qs.offset = 0; + + do { + responseData = await marketstackApiRequest.call(this, method, endpoint, body, qs); + returnData.push(...responseData.data); + + if (!returnAll && returnData.length > limit) { + return returnData.slice(0, limit); + } + + qs.offset += responseData.count; + } while ( + responseData.total > returnData.length + ); + + return returnData; +} + +export const format = (datetime?: string) => datetime?.split('T')[0]; + +export function validateTimeOptions( + this: IExecuteFunctions, + timeOptions: boolean[], +) { + if (timeOptions.every(o => !o)) { + throw new NodeOperationError( + this.getNode(), + 'Please filter by latest, specific date or timeframe (start and end dates).', + ); + } + + if (timeOptions.filter(Boolean).length > 1) { + throw new NodeOperationError( + this.getNode(), + 'Please filter by one of latest, specific date, or timeframe (start and end dates).', + ); + } +} diff --git a/packages/nodes-base/nodes/Marketstack/Marketstack.node.ts b/packages/nodes-base/nodes/Marketstack/Marketstack.node.ts new file mode 100644 index 0000000000..325855713a --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/Marketstack.node.ts @@ -0,0 +1,203 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + NodeOperationError, +} from 'n8n-workflow'; + +import { + endOfDayDataFields, + endOfDayDataOperations, + exchangeFields, + exchangeOperations, + tickerFields, + tickerOperations, +} from './descriptions'; + +import { + format, + marketstackApiRequest, + marketstackApiRequestAllItems, + validateTimeOptions, +} from './GenericFunctions'; + +import { + EndOfDayDataFilters, + Operation, + Resource, +} from './types'; + +export class Marketstack implements INodeType { + description: INodeTypeDescription = { + displayName: 'Marketstack', + name: 'marketstack', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + icon: 'file:marketstack.svg', + group: ['transform'], + version: 1, + description: 'Consume Marketstack API', + defaults: { + name: 'Marketstack', + color: '#02283e', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'marketstackApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'End-of-Day Data', + value: 'endOfDayData', + description: 'Stock market closing data', + }, + { + name: 'Exchange', + value: 'exchange', + description: 'Stock market exchange', + }, + { + name: 'Ticker', + value: 'ticker', + description: 'Stock market symbol', + }, + ], + default: 'endOfDayData', + required: true, + }, + ...endOfDayDataOperations, + ...endOfDayDataFields, + ...exchangeOperations, + ...exchangeFields, + ...tickerOperations, + ...tickerFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as Resource; + const operation = this.getNodeParameter('operation', 0) as Operation; + + let responseData: any; // tslint:disable-line: no-any + const returnData: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'endOfDayData') { + + if (operation === 'getAll') { + + // ---------------------------------- + // endOfDayData: getAll + // ---------------------------------- + + const qs: IDataObject = { + symbols: this.getNodeParameter('symbols', i), + }; + + const { + latest, + specificDate, + dateFrom, + dateTo, + ...rest + } = this.getNodeParameter('filters', i) as EndOfDayDataFilters; + + validateTimeOptions.call(this, [ + latest !== undefined && latest !== false, + specificDate !== undefined, + dateFrom !== undefined && dateTo !== undefined, + ]); + + if (Object.keys(rest).length) { + Object.assign(qs, rest); + } + + let endpoint: string; + + if (latest) { + endpoint = '/eod/latest'; + } else if (specificDate) { + endpoint = `/eod/${format(specificDate)}`; + } else { + if (!dateFrom || !dateTo) { + throw new NodeOperationError( + this.getNode(), + 'Please enter a start and end date to filter by timeframe.', + ); + } + endpoint = '/eod'; + qs.date_from = format(dateFrom); + qs.date_to = format(dateTo); + } + + responseData = await marketstackApiRequestAllItems.call(this, 'GET', endpoint, {}, qs); + + } + + } else if (resource === 'exchange') { + + if (operation === 'get') { + + // ---------------------------------- + // exchange: get + // ---------------------------------- + + const exchange = this.getNodeParameter('exchange', i); + const endpoint = `/exchanges/${exchange}`; + + responseData = await marketstackApiRequest.call(this, 'GET', endpoint); + + } + + } else if (resource === 'ticker') { + + if (operation === 'get') { + + // ---------------------------------- + // ticker: get + // ---------------------------------- + + const symbol = this.getNodeParameter('symbol', i); + const endpoint = `/tickers/${symbol}`; + + responseData = await marketstackApiRequest.call(this, 'GET', endpoint); + + } + + } + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Marketstack/descriptions/EndOfDayDataDescription.ts b/packages/nodes-base/nodes/Marketstack/descriptions/EndOfDayDataDescription.ts new file mode 100644 index 0000000000..7b36201441 --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/descriptions/EndOfDayDataDescription.ts @@ -0,0 +1,157 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const endOfDayDataOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'getAll', + displayOptions: { + show: { + resource: [ + 'endOfDayData', + ], + }, + }, + }, +]; + +export const endOfDayDataFields: INodeProperties[] = [ + { + displayName: 'Ticker', + name: 'symbols', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'endOfDayData', + ], + operation: [ + 'getAll', + ], + }, + }, + default: '', + description: 'One or multiple comma-separated stock symbols (tickers) to retrieve, e.g. AAPL or AAPL,MSFT', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'endOfDayData', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'endOfDayData', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'endOfDayData', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Exchange', + name: 'exchange', + type: 'string', + default: '', + description: 'Stock exchange to filter results by, specified by Market Identifier Code, e.g. XNAS', + }, + { + displayName: 'Latest', + name: 'latest', + type: 'boolean', + default: false, + description: 'Whether to fetch the most recent stock market data', + }, + { + displayName: 'Sort Order', + name: 'sort', + description: 'Order to sort results in', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ASC', + }, + { + name: 'Descending', + value: 'DESC', + }, + ], + default: 'DESC', + }, + { + displayName: 'Specific Date', + name: 'specificDate', + type: 'dateTime', + default: '', + description: 'Date in YYYY-MM-DD format, e.g. 2020-01-01, or in ISO-8601 date format, e.g. 2020-05-21T00:00:00+0000', + }, + { + displayName: 'Timeframe Start Date', + name: 'dateFrom', + type: 'dateTime', + default: '', + description: 'Timeframe start date in YYYY-MM-DD format, e.g. 2020-01-01, or in ISO-8601 date format, e.g. 2020-05-21T00:00:00+0000', + }, + { + displayName: 'Timeframe End Date', + name: 'dateTo', + type: 'dateTime', + default: '', + description: 'Timeframe end date in YYYY-MM-DD format, e.g. 2020-01-01, or in ISO-8601 date format, e.g. 2020-05-21T00:00:00+0000', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Marketstack/descriptions/ExchangeDescription.ts b/packages/nodes-base/nodes/Marketstack/descriptions/ExchangeDescription.ts new file mode 100644 index 0000000000..5973020ab7 --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/descriptions/ExchangeDescription.ts @@ -0,0 +1,46 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const exchangeOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Get', + value: 'get', + }, + ], + default: 'get', + displayOptions: { + show: { + resource: [ + 'exchange', + ], + }, + }, + }, +]; + +export const exchangeFields: INodeProperties[] = [ + { + displayName: 'Exchange', + name: 'exchange', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'exchange', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Stock exchange to retrieve, specified by Market Identifier Code, e.g. XNAS', + }, +]; diff --git a/packages/nodes-base/nodes/Marketstack/descriptions/TickerDescription.ts b/packages/nodes-base/nodes/Marketstack/descriptions/TickerDescription.ts new file mode 100644 index 0000000000..d0e8839f05 --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/descriptions/TickerDescription.ts @@ -0,0 +1,46 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const tickerOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Get', + value: 'get', + }, + ], + default: 'get', + displayOptions: { + show: { + resource: [ + 'ticker', + ], + }, + }, + }, +]; + +export const tickerFields: INodeProperties[] = [ + { + displayName: 'Ticker', + name: 'symbol', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'ticker', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Stock symbol (ticker) to retrieve, e.g. AAPL', + }, +]; diff --git a/packages/nodes-base/nodes/Marketstack/descriptions/index.ts b/packages/nodes-base/nodes/Marketstack/descriptions/index.ts new file mode 100644 index 0000000000..8015eaae6f --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/descriptions/index.ts @@ -0,0 +1,3 @@ +export * from './EndOfDayDataDescription'; +export * from './TickerDescription'; +export * from './ExchangeDescription'; diff --git a/packages/nodes-base/nodes/Marketstack/marketstack.svg b/packages/nodes-base/nodes/Marketstack/marketstack.svg new file mode 100644 index 0000000000..25ad681cc9 --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/marketstack.svg @@ -0,0 +1,57 @@ + + + + + +Marketstack + + +Marketstack diff --git a/packages/nodes-base/nodes/Marketstack/types.d.ts b/packages/nodes-base/nodes/Marketstack/types.d.ts new file mode 100644 index 0000000000..17760df15f --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/types.d.ts @@ -0,0 +1,12 @@ +export type Resource = 'endOfDayData' | 'exchange' | 'ticker'; + +export type Operation = 'get' | 'getAll'; + +export type EndOfDayDataFilters = { + latest?: boolean; + sort?: 'ASC' | 'DESC'; + specificDate?: string; + dateFrom?: string; + dateTo?: string; + exchange?: string; +}; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 1448760dce..742f244014 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -155,6 +155,7 @@ "dist/credentials/MailjetEmailApi.credentials.js", "dist/credentials/MailjetSmsApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", + "dist/credentials/MarketstackApi.credentials.js", "dist/credentials/MatrixApi.credentials.js", "dist/credentials/MattermostApi.credentials.js", "dist/credentials/MauticApi.credentials.js", @@ -452,6 +453,7 @@ "dist/nodes/Mailjet/Mailjet.node.js", "dist/nodes/Mailjet/MailjetTrigger.node.js", "dist/nodes/Mandrill/Mandrill.node.js", + "dist/nodes/Marketstack/Marketstack.node.js", "dist/nodes/Matrix/Matrix.node.js", "dist/nodes/Mattermost/Mattermost.node.js", "dist/nodes/Mautic/Mautic.node.js",