diff --git a/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts b/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts new file mode 100644 index 0000000000..0ebb10ab7e --- /dev/null +++ b/packages/nodes-base/credentials/ElasticsearchApi.credentials.ts @@ -0,0 +1,35 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class ElasticsearchApi implements ICredentialType { + name = 'elasticsearchApi'; + displayName = 'Elasticsearch API'; + documentationUrl = 'elasticsearch'; + properties = [ + { + displayName: 'Username', + name: 'username', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + }, + { + displayName: 'Base URL', + name: 'baseUrl', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'https://abc.elastic-cloud.com:9243', + description: 'Referred to as \'endpoint\' in the Elasticsearch dashboard.', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Elasticsearch/Elasticsearch.node.ts b/packages/nodes-base/nodes/Elasticsearch/Elasticsearch.node.ts new file mode 100644 index 0000000000..e28ff8cb0b --- /dev/null +++ b/packages/nodes-base/nodes/Elasticsearch/Elasticsearch.node.ts @@ -0,0 +1,350 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + elasticsearchApiRequest, +} from './GenericFunctions'; + +import { + documentFields, + documentOperations, + indexFields, + indexOperations, +} from './descriptions'; + +import { + DocumentGetAllOptions, + FieldsUiValues, +} from './types'; + +import { + omit, +} from 'lodash'; + +export class Elasticsearch implements INodeType { + description: INodeTypeDescription = { + displayName: 'Elasticsearch', + name: 'elasticsearch', + icon: 'file:elasticsearch.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Elasticsearch API', + defaults: { + name: 'Elasticsearch', + color: '#f3d337', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'elasticsearchApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Document', + value: 'document', + }, + { + name: 'Index', + value: 'index', + }, + ], + default: 'document', + description: 'Resource to consume', + }, + ...documentOperations, + ...documentFields, + ...indexOperations, + ...indexFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as 'document' | 'index'; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + + for (let i = 0; i < items.length; i++) { + + if (resource === 'document') { + + // ********************************************************************** + // document + // ********************************************************************** + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/docs.html + + if (operation === 'delete') { + + // ---------------------------------------- + // document: delete + // ---------------------------------------- + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html + + const indexId = this.getNodeParameter('indexId', i); + const documentId = this.getNodeParameter('documentId', i); + + const endpoint = `/${indexId}/_doc/${documentId}`; + responseData = await elasticsearchApiRequest.call(this, 'DELETE', endpoint); + + } else if (operation === 'get') { + + // ---------------------------------------- + // document: get + // ---------------------------------------- + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html + + const indexId = this.getNodeParameter('indexId', i); + const documentId = this.getNodeParameter('documentId', i); + + const qs = {} as IDataObject; + const options = this.getNodeParameter('options', i) as IDataObject; + + if (Object.keys(options).length) { + Object.assign(qs, options); + qs._source = true; + } + + const endpoint = `/${indexId}/_doc/${documentId}`; + responseData = await elasticsearchApiRequest.call(this, 'GET', endpoint, {}, qs); + + const simple = this.getNodeParameter('simple', i) as IDataObject; + + if (simple) { + responseData = responseData._source; + } + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // document: getAll + // ---------------------------------------- + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html + + const indexId = this.getNodeParameter('indexId', i); + + const body = {} as IDataObject; + const qs = {} as IDataObject; + const options = this.getNodeParameter('options', i) as DocumentGetAllOptions; + + if (Object.keys(options).length) { + const { query, ...rest } = options; + if (query) Object.assign(body, JSON.parse(query)); + Object.assign(qs, rest); + qs._source = true; + } + + const returnAll = this.getNodeParameter('returnAll', 0); + + if (!returnAll) { + qs.size = this.getNodeParameter('limit', 0); + } + + responseData = await elasticsearchApiRequest.call(this, 'GET', `/${indexId}/_search`, body, qs); + responseData = responseData.hits.hits; + + } else if (operation === 'create') { + + // ---------------------------------------- + // document: create + // ---------------------------------------- + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html + + const body: IDataObject = {}; + + const dataToSend = this.getNodeParameter('dataToSend', 0) as 'defineBelow' | 'autoMapInputData'; + + if (dataToSend === 'defineBelow') { + + const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues; + fields.forEach(({ fieldId, fieldValue }) => body[fieldId] = fieldValue); + + } else { + + const inputData = items[i].json; + const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string; + const inputsToIgnore = rawInputsToIgnore.split(',').map(c => c.trim()); + + for (const key of Object.keys(inputData)) { + if (inputsToIgnore.includes(key)) continue; + body[key] = inputData[key]; + } + + } + + const qs = {} as IDataObject; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(qs, omit(additionalFields, ['documentId'])); + } + + const indexId = this.getNodeParameter('indexId', i); + const { documentId } = additionalFields; + + if (documentId) { + const endpoint = `/${indexId}/_doc/${documentId}`; + responseData = await elasticsearchApiRequest.call(this, 'PUT', endpoint, body); + } else { + const endpoint = `/${indexId}/_doc`; + responseData = await elasticsearchApiRequest.call(this, 'POST', endpoint, body); + } + + } else if (operation === 'update') { + + // ---------------------------------------- + // document: update + // ---------------------------------------- + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html + + const body = { doc: {} } as { doc: { [key: string]: string } }; + + const dataToSend = this.getNodeParameter('dataToSend', 0) as 'defineBelow' | 'autoMapInputData'; + + if (dataToSend === 'defineBelow') { + + const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues; + fields.forEach(({ fieldId, fieldValue }) => body.doc[fieldId] = fieldValue); + + } else { + + const inputData = items[i].json; + const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string; + const inputsToIgnore = rawInputsToIgnore.split(',').map(c => c.trim()); + + for (const key of Object.keys(inputData)) { + if (inputsToIgnore.includes(key)) continue; + body.doc[key] = inputData[key] as string; + } + + } + + const indexId = this.getNodeParameter('indexId', i); + const documentId = this.getNodeParameter('documentId', i); + + const endpoint = `/${indexId}/_update/${documentId}`; + responseData = await elasticsearchApiRequest.call(this, 'POST', endpoint, body); + + } + + } else if (resource === 'index') { + + // ********************************************************************** + // index + // ********************************************************************** + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices.html + + if (operation === 'create') { + + // ---------------------------------------- + // index: create + // ---------------------------------------- + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html + + const indexId = this.getNodeParameter('indexId', i); + + const body = {} as IDataObject; + const qs = {} as IDataObject; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + const { aliases, mappings, settings, ...rest } = additionalFields; + Object.assign(body, aliases, mappings, settings); + Object.assign(qs, rest); + } + + responseData = await elasticsearchApiRequest.call(this, 'PUT', `/${indexId}`); + responseData = { id: indexId, ...responseData }; + delete responseData.index; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // index: delete + // ---------------------------------------- + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html + + const indexId = this.getNodeParameter('indexId', i); + + responseData = await elasticsearchApiRequest.call(this, 'DELETE', `/${indexId}`); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------------- + // index: get + // ---------------------------------------- + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-index.html + + const indexId = this.getNodeParameter('indexId', i) as string; + + const qs = {} as IDataObject; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(qs, additionalFields); + } + + responseData = await elasticsearchApiRequest.call(this, 'GET', `/${indexId}`, {}, qs); + responseData = { id: indexId, ...responseData[indexId] }; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // index: getAll + // ---------------------------------------- + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html + + responseData = await elasticsearchApiRequest.call(this, 'GET', '/_aliases'); + responseData = Object.keys(responseData).map(i => ({ indexId: i })); + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + + } + + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Elasticsearch/GenericFunctions.ts b/packages/nodes-base/nodes/Elasticsearch/GenericFunctions.ts new file mode 100644 index 0000000000..9fc7a9844b --- /dev/null +++ b/packages/nodes-base/nodes/Elasticsearch/GenericFunctions.ts @@ -0,0 +1,58 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + NodeApiError, +} from 'n8n-workflow'; + +import { + ElasticsearchApiCredentials, +} from './types'; + +export async function elasticsearchApiRequest( + this: IExecuteFunctions, + method: 'GET' | 'PUT' | 'POST' | 'DELETE', + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const { + username, + password, + baseUrl, + } = this.getCredentials('elasticsearchApi') as ElasticsearchApiCredentials; + + const token = Buffer.from(`${username}:${password}`).toString('base64'); + + const options: OptionsWithUri = { + headers: { + Authorization: `Basic ${token}`, + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: `${baseUrl}${endpoint}`, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + try { + return await this.helpers.request(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} diff --git a/packages/nodes-base/nodes/Elasticsearch/descriptions/DocumentDescription.ts b/packages/nodes-base/nodes/Elasticsearch/descriptions/DocumentDescription.ts new file mode 100644 index 0000000000..dca8122caa --- /dev/null +++ b/packages/nodes-base/nodes/Elasticsearch/descriptions/DocumentDescription.ts @@ -0,0 +1,771 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import * as placeholders from './placeholders'; + +export const documentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'document', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a document', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a document', + }, + { + name: 'Get', + value: 'get', + description: 'Get a document', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all documents', + }, + { + name: 'Update', + value: 'update', + description: 'Update a document', + }, + ], + default: 'get', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const documentFields = [ + // ---------------------------------------- + // document: delete + // ---------------------------------------- + { + displayName: 'Index ID', + name: 'indexId', + description: 'ID of the index containing the document to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'delete', + ], + }, + }, + }, + { + displayName: 'Document ID', + name: 'documentId', + description: 'ID of the document to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // document: get + // ---------------------------------------- + { + displayName: 'Index ID', + name: 'indexId', + description: 'ID of the index containing the document to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Document ID', + name: 'documentId', + description: 'ID of the document to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + default: true, + description: 'Whether to return a simplified version of the response instead of the raw data', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Source Excludes', + name: '_source_excludes', + description: 'Comma-separated list of source fields to exclude from the response', + type: 'string', + default: '', + }, + { + displayName: 'Source Includes', + name: '_source_includes', + description: 'Comma-separated list of source fields to include in the response', + type: 'string', + default: '', + }, + { + displayName: 'Stored Fields', + name: 'stored_fields', + description: 'If true, retrieve the document fields stored in the index rather than the document _source. Defaults to false', + type: 'boolean', + default: false, + }, + ], + }, + + // ---------------------------------------- + // document: getAll + // ---------------------------------------- + { + displayName: 'Index ID', + name: 'indexId', + description: 'ID of the index containing the documents to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + 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: [ + 'document', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Allow No Indices', + name: 'allow_no_indices', + description: 'If false, return an error if any of the following targets only missing/closed indices: wildcard expression, index alias, or _all value. Defaults to true', + type: 'boolean', + default: true, + }, + { + displayName: 'Allow Partial Search Results', + name: 'allow_partial_search_results', + description: 'If true, return partial results if there are shard request timeouts or shard failures.
If false, returns an error with no partial results. Defaults to true', + type: 'boolean', + default: true, + }, + { + displayName: 'Batched Reduce Size', + name: 'batched_reduce_size', + description: 'Number of shard results that should be reduced at once on the coordinating node. Defaults to 512', + type: 'number', + typeOptions: { + minValue: 2, + }, + default: 512, + }, + { + displayName: 'CCS Minimize Roundtrips', + name: 'ccs_minimize_roundtrips', + description: 'If true, network round-trips between the coordinating node and the remote clusters are minimized when executing cross-cluster search (CCS) requests. Defaults to true', + type: 'boolean', + default: true, + }, + { + displayName: 'Doc Value Fields', + name: 'docvalue_fields', + description: 'Comma-separated list of fields to return as the docvalue representation of a field for each hit', + type: 'string', + default: '', + }, + { + displayName: 'Expand Wildcards', + name: 'expand_wildcards', + description: 'Type of index that wildcard expressions can match. Defaults to open', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + }, + { + name: 'Closed', + value: 'closed', + }, + { + name: 'Hidden', + value: 'hidden', + }, + { + name: 'None', + value: 'none', + }, + { + name: 'Open', + value: 'open', + }, + ], + default: 'open', + }, + { + displayName: 'Explain', + name: 'explain', + description: 'If true, return detailed information about score computation as part of a hit. Defaults to false', + type: 'boolean', + default: false, + }, + { + displayName: 'Ignore Throttled', + name: 'ignore_throttled', + description: 'If true, concrete, expanded or aliased indices are ignored when frozen. Defaults to true', + type: 'boolean', + default: true, + }, + { + displayName: 'Ignore Unavailable', + name: 'ignore_unavailable', + description: 'If true, missing or closed indices are not included in the response. Defaults to false', + type: 'boolean', + default: false, + }, + { + displayName: 'Max Concurrent Shard Requests', + name: 'max_concurrent_shard_requests', + description: 'Define the number of shard requests per node this search executes concurrently. Defaults to 5', + type: 'number', + default: 5, + }, + { + displayName: 'Pre-Filter Shard Size', + name: 'pre_filter_shard_size', + description: 'Define a threshold that enforces a pre-filter roundtrip to prefilter search shards based on query rewriting.
Only used if the number of shards the search request expands to exceeds the threshold', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + }, + { + displayName: 'Query', + name: 'query', + description: 'Query in the Elasticsearch Query DSL', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + placeholder: placeholders.query, + }, + { + displayName: 'Request Cache', + name: 'request_cache', + description: 'If true, the caching of search results is enabled for requests where size is 0. See Elasticsearch shard request cache settings', + type: 'boolean', + default: false, + }, + { + displayName: 'Routing', + name: 'routing', + description: 'Target this primary shard', + type: 'string', + default: '', + }, + { + displayName: 'Search Type', + name: 'search_type', + description: 'How distributed term frequencies are calculated for relevance scoring. Defaults to Query then Fetch', + type: 'options', + options: [ + { + name: 'DFS Query Then Fetch', + value: 'dfs_query_then_fetch', + }, + { + name: 'Query Then Fetch', + value: 'query_then_fetch', + }, + ], + default: 'query_then_fetch', + }, + { + displayName: 'Sequence Number and Primary Term', + name: 'seq_no_primary_term', + description: 'If true, return the sequence number and primary term of the last modification of each hit. See Optimistic concurrency control', + type: 'boolean', + default: false, + }, + { + displayName: 'Sort', + name: 'sort', + description: 'Comma-separated list of field:direction pairs', + type: 'string', + default: '', + }, + { + displayName: 'Stats', + name: 'stats', + description: 'Tag of the request for logging and statistical purposes', + type: 'string', + default: '', + }, + { + displayName: 'Stored Fields', + name: 'stored_fields', + description: 'If true, retrieve the document fields stored in the index rather than the document _source. Defaults to false', + type: 'boolean', + default: false, + }, + { + displayName: 'Terminate After', + name: 'terminate_after', + description: 'Max number of documents to collect for each shard', + type: 'number', + default: 0, + }, + { + displayName: 'Timeout', + name: 'timeout', + description: 'Period to wait for active shards. Defaults to 1m (one minute). See the Elasticsearch time units reference', + type: 'string', + default: '1m', + }, + { + displayName: 'Track Scores', + name: 'track_scores', + description: 'If true, calculate and return document scores, even if the scores are not used for sorting. Defaults to false', + type: 'boolean', + default: false, + }, + { + displayName: 'Track Total Hits', + name: 'track_total_hits', + description: 'Number of hits matching the query to count accurately. Defaults to 10000', + type: 'number', + default: 10000, + }, + { + displayName: 'Version', + name: 'version', + description: 'If true, return document version as part of a hit. Defaults to false', + type: 'boolean', + default: false, + }, + ], + }, + + // ---------------------------------------- + // document: create + // ---------------------------------------- + { + displayName: 'Index ID', + name: 'indexId', + description: 'ID of the index to add the document to', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Data to Send', + name: 'dataToSend', + type: 'options', + options: [ + { + name: 'Define Below for Each Column', + value: 'defineBelow', + description: 'Set the value for each destination column', + }, + { + name: 'Auto-map Input Data to Columns', + value: 'autoMapInputData', + description: 'Use when node input properties match destination column names', + }, + ], + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'create', + ], + }, + }, + default: 'defineBelow', + description: 'Whether to insert the input data this node receives in the new row', + }, + { + displayName: 'Inputs to Ignore', + name: 'inputsToIgnore', + type: 'string', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'create', + ], + dataToSend: [ + 'autoMapInputData', + ], + }, + }, + default: '', + required: false, + description: 'List of input properties to avoid sending, separated by commas. Leave empty to send all properties', + placeholder: 'Enter properties...', + }, + { + displayName: 'Fields to Send', + name: 'fieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValueButtonText: 'Add Field to Send', + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'create', + ], + dataToSend: [ + 'defineBelow', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'fieldValues', + values: [ + { + displayName: 'Field Name', + name: 'fieldId', + type: 'string', + default: '', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Document ID', + name: 'documentId', + description: 'ID of the document to create and add to the index', + type: 'string', + default: '', + }, + { + displayName: 'Routing', + name: 'routing', + description: 'Target this primary shard', + type: 'string', + default: '', + }, + { + displayName: 'Timeout', + name: 'timeout', + description: 'Period to wait for active shards. Defaults to 1m (one minute). See the Elasticsearch time units reference', + type: 'string', + default: '1m', + }, + ], + }, + + // ---------------------------------------- + // document: update + // ---------------------------------------- + { + displayName: 'Index ID', + name: 'indexId', + description: 'ID of the document to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Document ID', + name: 'documentId', + description: 'ID of the document to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Data to Send', + name: 'dataToSend', + type: 'options', + options: [ + { + name: 'Define Below for Each Column', + value: 'defineBelow', + description: 'Set the value for each destination column', + }, + { + name: 'Auto-map Input Data to Columns', + value: 'autoMapInputData', + description: 'Use when node input properties match destination column names', + }, + ], + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'update', + ], + }, + }, + default: 'defineBelow', + description: 'Whether to insert the input data this node receives in the new row', + }, + { + displayName: 'Inputs to Ignore', + name: 'inputsToIgnore', + type: 'string', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'update', + ], + dataToSend: [ + 'autoMapInputData', + ], + }, + }, + default: '', + required: false, + description: 'List of input properties to avoid sending, separated by commas. Leave empty to send all properties', + placeholder: 'Enter properties...', + }, + { + displayName: 'Fields to Send', + name: 'fieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValueButtonText: 'Add Field to Send', + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'update', + ], + dataToSend: [ + 'defineBelow', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'fieldValues', + values: [ + { + displayName: 'Field Name', + name: 'fieldId', + type: 'string', + default: '', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + default: '', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Elasticsearch/descriptions/IndexDescription.ts b/packages/nodes-base/nodes/Elasticsearch/descriptions/IndexDescription.ts new file mode 100644 index 0000000000..504b0967c1 --- /dev/null +++ b/packages/nodes-base/nodes/Elasticsearch/descriptions/IndexDescription.ts @@ -0,0 +1,322 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import * as placeholders from './placeholders'; + +export const indexOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'index', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const indexFields = [ + // ---------------------------------------- + // index: create + // ---------------------------------------- + { + displayName: 'Index ID', + name: 'indexId', + description: 'ID of the index to create', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'index', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'index', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Aliases', + name: 'aliases', + description: 'Index aliases which include the index, as an alias object', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + placeholder: placeholders.aliases, + }, + { + displayName: 'Include Type Name', + name: 'include_type_name', + description: 'If true, a mapping type is expected in the body of mappings. Defaults to false', + type: 'boolean', + default: false, + }, + { + displayName: 'Mappings', + name: 'mappings', + description: 'Mapping for fields in the index, as mapping object', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + placeholder: placeholders.mappings, + }, + { + displayName: 'Master Timeout', + name: 'master_timeout', + description: 'Period to wait for a connection to the master node. If no response is received before the timeout expires,
the request fails and returns an error. Defaults to 1m. See the Elasticsearch time units reference', + type: 'string', + default: '1m', + }, + { + displayName: 'Settings', + name: 'settings', + description: 'Configuration options for the index, as an index settings object', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + placeholder: placeholders.indexSettings, + }, + { + displayName: 'Timeout', + name: 'timeout', + description: 'Period to wait for a response. If no response is received before the timeout expires, the request
fails and returns an error. Defaults to 30s. See the Elasticsearch time units reference', + type: 'string', + default: '30s', + }, + { + displayName: 'Wait for Active Shards', + name: 'wait_for_active_shards', + description: 'The number of shard copies that must be active before proceeding with the operation. Set to all
or any positive integer up to the total number of shards in the index. Default: 1, the primary shard', + type: 'string', + default: '1', + }, + ], + }, + + // ---------------------------------------- + // index: delete + // ---------------------------------------- + { + displayName: 'Index ID', + name: 'indexId', + description: 'ID of the index to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'index', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // index: get + // ---------------------------------------- + { + displayName: 'Index ID', + name: 'indexId', + description: 'ID of the index to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'index', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'index', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Allow No Indices', + name: 'allow_no_indices', + description: 'If false, return an error if any of the following targets only missing/closed indices: wildcard expression, index alias, or _all value. Defaults to true', + type: 'boolean', + default: true, + }, + { + displayName: 'Expand Wildcards', + name: 'expand_wildcards', + description: 'Type of index that wildcard expressions can match. Defaults to open', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + }, + { + name: 'Closed', + value: 'closed', + }, + { + name: 'Hidden', + value: 'hidden', + }, + { + name: 'None', + value: 'none', + }, + { + name: 'Open', + value: 'open', + }, + ], + default: 'all', + }, + { + displayName: 'Flat Settings', + name: 'flat_settings', + description: 'If true, return settings in flat format. Defaults to false', + type: 'boolean', + default: false, + }, + { + displayName: 'Ignore Unavailable', + name: 'ignore_unavailable', + description: 'If false, requests that target a missing index return an error. Defaults to false', + type: 'boolean', + default: false, + }, + { + displayName: 'Include Defaults', + name: 'include_defaults', + description: 'If true, return all default settings in the response. Defaults to false', + type: 'boolean', + default: false, + }, + { + displayName: 'Local', + name: 'local', + description: 'If true, retrieve information from the local node only. Defaults to false', + type: 'boolean', + default: false, + }, + { + displayName: 'Master Timeout', + name: 'master_timeout', + description: 'Period to wait for a connection to the master node. If no response is received before the timeout expires,
the request fails and returns an error. Defaults to 1m. See the Elasticsearch time units reference', + type: 'string', + default: '1m', + }, + ], + }, + + // ---------------------------------------- + // index: getAll + // ---------------------------------------- + { + 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: [ + 'index', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'index', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Elasticsearch/descriptions/index.ts b/packages/nodes-base/nodes/Elasticsearch/descriptions/index.ts new file mode 100644 index 0000000000..f20c261442 --- /dev/null +++ b/packages/nodes-base/nodes/Elasticsearch/descriptions/index.ts @@ -0,0 +1,2 @@ +export * from './DocumentDescription'; +export * from './IndexDescription'; diff --git a/packages/nodes-base/nodes/Elasticsearch/descriptions/placeholders.ts b/packages/nodes-base/nodes/Elasticsearch/descriptions/placeholders.ts new file mode 100644 index 0000000000..bda913525a --- /dev/null +++ b/packages/nodes-base/nodes/Elasticsearch/descriptions/placeholders.ts @@ -0,0 +1,43 @@ +export const indexSettings = `{ + "settings": { + "index": { + "number_of_shards": 3, + "number_of_replicas": 2 + } + } +}`; + +export const mappings = `{ + "mappings": { + "properties": { + "field1": { "type": "text" } + } + } +}`; + +export const aliases = `{ + "aliases": { + "alias_1": {}, + "alias_2": { + "filter": { + "term": { "user.id": "kimchy" } + }, + "routing": "shard-1" + } + } +}`; + +export const query = `{ + "query": { + "term": { + "user.id": "john" + } + } +}`; + +export const document = `{ + "timestamp": "2099-05-06T16:21:15.000Z", + "event": { + "original": "192.0.2.42 - - [06/May/2099:16:21:15 +0000] \"GET /images/bg.jpg HTTP/1.0\" 200 24736" + } +}`; diff --git a/packages/nodes-base/nodes/Elasticsearch/elasticsearch.svg b/packages/nodes-base/nodes/Elasticsearch/elasticsearch.svg new file mode 100644 index 0000000000..7e2fad792c --- /dev/null +++ b/packages/nodes-base/nodes/Elasticsearch/elasticsearch.svg @@ -0,0 +1,14 @@ + + + + elastic-search-logo-color-64px + Created with Sketch. + + + + + + + + + diff --git a/packages/nodes-base/nodes/Elasticsearch/types.d.ts b/packages/nodes-base/nodes/Elasticsearch/types.d.ts new file mode 100644 index 0000000000..105766301e --- /dev/null +++ b/packages/nodes-base/nodes/Elasticsearch/types.d.ts @@ -0,0 +1,40 @@ +export type ElasticsearchApiCredentials = { + username: string; + password: string; + baseUrl: string; +}; + +export type DocumentGetAllOptions = Partial<{ + allow_no_indices: boolean; + allow_partial_search_results: boolean; + batched_reduce_size: number; + ccs_minimize_roundtrips: boolean; + docvalue_fields: string; + expand_wildcards: 'All' | 'Closed' | 'Hidden' | 'None' | 'Open'; + explain: boolean; + ignore_throttled: boolean; + ignore_unavailable: boolean; + max_concurrent_shard_requests: number; + pre_filter_shard_size: number; + query: string; + request_cache: boolean; + routing: string; + search_type: 'query_then_fetch' | 'dfs_query_then_fetch'; + seq_no_primary_term: boolean; + sort: string; + _source: boolean; + _source_excludes: string; + _source_includes: string; + stats: string; + stored_fields: boolean; + terminate_after: boolean; + timeout: number; + track_scores: boolean; + track_total_hits: string; + version: boolean; +}>; + +export type FieldsUiValues = Array<{ + fieldId: string; + fieldValue: string; +}>; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 7bbd5fc8f0..f4aacdc008 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -76,6 +76,7 @@ "dist/credentials/DropboxApi.credentials.js", "dist/credentials/DropboxOAuth2Api.credentials.js", "dist/credentials/EgoiApi.credentials.js", + "dist/credentials/ElasticsearchApi.credentials.js", "dist/credentials/EmeliaApi.credentials.js", "dist/credentials/ERPNextApi.credentials.js", "dist/credentials/EventbriteApi.credentials.js", @@ -355,6 +356,7 @@ "dist/nodes/Dropbox/Dropbox.node.js", "dist/nodes/EditImage.node.js", "dist/nodes/Egoi/Egoi.node.js", + "dist/nodes/Elasticsearch/Elasticsearch.node.js", "dist/nodes/EmailReadImap.node.js", "dist/nodes/EmailSend.node.js", "dist/nodes/Emelia/Emelia.node.js",