From 523576ce96e618698f43f4e45d54ea029ef73985 Mon Sep 17 00:00:00 2001 From: Steven Milanese Date: Fri, 7 Mar 2025 04:27:10 -0500 Subject: [PATCH] Initial commit --- .gitattributes | 1 + .gitignore | 5 + .npmignore | 5 + LICENSE.md | 21 ++ README.md | 164 +++++++++++++ package.json | 43 ++++ src/credentials/TavilyApi.credentials.ts | 18 ++ src/index.ts | 6 + src/nodes/Tavily/TavilyExtract.node.ts | 130 ++++++++++ src/nodes/Tavily/TavilySearch.node.ts | 291 +++++++++++++++++++++++ src/nodes/Tavily/tavilyApi.utils.ts | 85 +++++++ tsconfig.json | 15 ++ 12 files changed, 784 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 package.json create mode 100644 src/credentials/TavilyApi.credentials.ts create mode 100644 src/index.ts create mode 100644 src/nodes/Tavily/TavilyExtract.node.ts create mode 100644 src/nodes/Tavily/TavilySearch.node.ts create mode 100644 src/nodes/Tavily/tavilyApi.utils.ts create mode 100644 tsconfig.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..373a242 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +*.log +*.env +.DS_Store diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..90a3590 --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ +# Exclude source files and unneeded config files from the published package +src/ +.gitignore +.gitattributes +tsconfig.json diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f577de4 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) 2024 Steven Milanese + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d779f45 --- /dev/null +++ b/README.md @@ -0,0 +1,164 @@ +# n8n-tavily-nodes + +**n8n-tavily-nodes** is an n8n community node package that integrates the [Tavily API](https://api.tavily.com) for powerful **web search** and **content extraction**. + +It contains two nodes: + +1. **Tavily Search**: Query the web using Tavily’s `/search` endpoint. +2. **Tavily Extract**: Extract structured content from URLs using Tavily’s `/extract` endpoint. + +--- + +## Table of Contents + +1. [Features](#features) +2. [Installation](#installation) +3. [Getting a Tavily API Key](#getting-a-tavily-api-key) +4. [Configuring Credentials in n8n](#configuring-credentials-in-n8n) +5. [Usage](#usage) + * [Example: Tavily Search Node](#example-tavily-search-node) + * [Example: Tavily Extract Node](#example-tavily-extract-node) +6. [Parameters](#parameters) + * [Tavily Search Parameters](#tavily-search-parameters) + * [Tavily Extract Parameters](#tavily-extract-parameters) +7. [Troubleshooting](#troubleshooting) +8. [License (MIT)](#license-mit) + +--- + +## Features + +- **Tavily Search** + * Query the web with multiple filtering options (topic, time range, domain inclusion/exclusion, etc.). + * Optionally retrieve a generated answer, raw content, or images. + +- **Tavily Extract** + * Extract text and optional images from one or more URLs. + * Choose between basic or advanced extraction depth. + +- **Robust Error Handling** + * Comprehensive input validation with descriptive error messages. + * Catch Tavily API error codes (`400, 401, 403, 429, 500`) and display them in n8n. + +- **Easy Setup** + * Install from npm. + * Configure a single **Tavily API** credential in n8n. + +--- + +## Installation + +1. From your n8n root directory, run: + + ```bash + npm install n8n-tavily-nodes + ``` + +2. Restart n8n. + +--- + +## Getting a Tavily API Key + +1. Go to the [Tavily website](https://tavily.com) and create an account. +2. Navigate to your dashboard or settings to find your API key. + +--- + +## Configuring Credentials in n8n + +1. In your n8n instance, go to the "Credentials" section. +2. Click "Create Credential". +3. Search for or select "Tavily API". +4. Enter your Tavily API key in the appropriate field. +5. Save the credential. + +--- + +## Usage + +### Example: Tavily Search Node + +1. Add the "Tavily Search" node to your n8n workflow. +2. Connect it to the preceding node in your workflow. +3. In the node's settings: + * Select your Tavily API credential. + * Enter your search query. + * Configure any other desired search parameters (topic, search depth, etc.). +4. Run the workflow to execute the search. + +### Example: Tavily Extract Node + +1. Add the "Tavily Extract" node to your n8n workflow. +2. Connect it to the preceding node. +3. In the node's settings: + * Select your Tavily API credential. + * Enter the URLs you want to extract content from. + * Configure any other extraction parameters (include images, extract depth). +4. Run the workflow to extract the content. + +--- + +## Parameters + +### Tavily Search Parameters + +| Parameter | Description | +| :--------------------- | :------------------------------------------------------------------------------------------------------ | +| **Query** | The search query to execute. | +| **Topic** | The category of the search (General or News). | +| **Search Depth** | The depth of the search (Basic or Advanced). | +| **Max Results** | Maximum number of search results to return (0-20). | +| **Time Range** | Time range filter for results (relative to current date). | +| **Days (News Only)** | Number of days back from the current date to include (for News topic). | +| **Include Answer** | Include an LLM-generated answer in the response (No, Basic, or Advanced). | +| **Include Raw Content** | Include cleaned and parsed HTML content of each search result. | +| **Include Images** | Perform an image search and include the results in the response. | +| **Include Image Descriptions** | When including images, also add a descriptive text for each image. | +| **Include Domains** | A list of domains to specifically include in the search results. | +| **Exclude Domains** | A list of domains to specifically exclude from the search results. | + +### Tavily Extract Parameters + +| Parameter | Description | +| :--------------- | :-------------------------------------------------------------------------- | +| **URLs** | One or more URLs to extract content from. | +| **Include Images** | Include a list of images extracted from each URL. | +| **Extract Depth** | How deeply to parse each URL (Basic or Advanced). | + +--- + +## Troubleshooting + +### Common Issues & Error Codes + +* **Invalid API Key:** Ensure your Tavily API key is entered correctly in the credentials. +* **Rate Limiting (429):** You have exceeded your Tavily API rate limit. Wait a while before making more requests or upgrade your Tavily plan. +* **Bad Request (400):** Check your input parameters for errors or missing required fields. +* **Other Error Codes (401, 403, 500):** Refer to the Tavily API documentation for details on specific error codes. + +--- + +## License (MIT) + +MIT License + +Copyright (c) 2025 LEVEL AI XYZ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..1cc09fa --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "n8n-tavily-nodes", + "version": "1.0.0", + "description": "n8n Community Nodes for Tavily API integration", + "author": "Steven Milanese (https://StevenMilanese.com)", + "license": "MIT", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "n8n", + "community-node", + "tavily", + "api", + "search", + "extract" + ], + "repository": { + "type": "git", + "url": "https://github.com/developtheweb/n8n-tavily-nodes" + }, + "bugs": { + "url": "https://github.com/developtheweb/n8n-tavily-nodes/issues" + }, + "dependencies": { + "axios": "^1.6.0" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "typescript": "^4.9.0" + }, + "n8n": { + "credentials": [ + "./dist/credentials/TavilyApi.credentials.js" + ], + "nodes": [ + "./dist/nodes/Tavily/TavilySearch.node.js", + "./dist/nodes/Tavily/TavilyExtract.node.js" + ] + } +} diff --git a/src/credentials/TavilyApi.credentials.ts b/src/credentials/TavilyApi.credentials.ts new file mode 100644 index 0000000..2c0793c --- /dev/null +++ b/src/credentials/TavilyApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class TavilyApi implements ICredentialType { + name = 'tavilyApi'; + displayName = 'Tavily API'; + properties: INodeProperties[] = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string', + default: '', + description: 'Enter your Tavily API key here (e.g., tvly-abcdef123456)', + }, + ]; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..321044c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,6 @@ +// Exporting credentials +export * from './credentials/TavilyApi.credentials'; + +// Exporting nodes +export * from './nodes/Tavily/TavilySearch.node'; +export * from './nodes/Tavily/TavilyExtract.node'; diff --git a/src/nodes/Tavily/TavilyExtract.node.ts b/src/nodes/Tavily/TavilyExtract.node.ts new file mode 100644 index 0000000..1672a83 --- /dev/null +++ b/src/nodes/Tavily/TavilyExtract.node.ts @@ -0,0 +1,130 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + INodeExecutionData, + INodeType, + INodeTypeDescription, + INodeProperties, + NodeApiError, +} from 'n8n-workflow'; + +import { tavilyApiRequest } from './tavilyApi.utils'; + +export class TavilyExtract implements INodeType { + description: INodeTypeDescription = { + displayName: 'Tavily Extract', + name: 'tavilyExtract', + icon: 'file:tavily.svg', + group: ['transform'], + version: 1, + description: 'Extract web page content using the Tavily Extract endpoint', + defaults: { + name: 'Tavily Extract', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'tavilyApi', + required: true, + }, + ], + properties: [ + { + displayName: 'URLs', + name: 'urls', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add URL', + }, + default: [], + required: true, + description: 'One or more URLs to extract content from', + }, + { + displayName: 'Include Images', + name: 'includeImages', + type: 'boolean', + default: false, + description: 'Include a list of images extracted from each URL', + }, + { + displayName: 'Extract Depth', + name: 'extractDepth', + type: 'options', + options: [ + { name: 'Basic', value: 'basic' }, + { name: 'Advanced', value: 'advanced' }, + ], + default: 'basic', + description: 'How deeply to parse each URL. "advanced" retrieves more data but can increase latency', + }, + { + displayName: 'API Documentation', + name: 'apiDocumentationNotice', + type: 'notice', + default: '', + description: `### Tavily Extract API + +**Endpoint**: POST /extract +**Body Parameters**: +- urls (string[]; required) +- include_images (boolean) +- extract_depth (basic|advanced) + +**Response**: +- results +- failed_results +- response_time +`, + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const credentials = await this.getCredentials('tavilyApi'); + if (!credentials?.apiKey) { + throw new NodeApiError(this.getNode(), new Error('Missing Tavily API key in credentials.')); + } + const apiKey = credentials.apiKey as string; + + for (let i = 0; i < items.length; i++) { + try { + const urls = this.getNodeParameter('urls', i, []) as string[]; + const includeImages = this.getNodeParameter('includeImages', i) as boolean; + const extractDepth = this.getNodeParameter('extractDepth', i) as string; + + // Validate input parameters + if (!Array.isArray(urls) || urls.length === 0) { + throw new NodeApiError(this.getNode(), new Error('At least one URL is required.')); + } + + const body = { + urls, + include_images: includeImages, + extract_depth: extractDepth, + }; + + const result = await tavilyApiRequest.call( + this, + 'POST', + '/extract', + body, + apiKey, + ); + + returnData.push({ json: result }); + } catch (error) { + if (error instanceof NodeApiError) { + throw error; + } + throw new NodeApiError(this.getNode(), error as Error); + } + } + + return [returnData]; + } +} diff --git a/src/nodes/Tavily/TavilySearch.node.ts b/src/nodes/Tavily/TavilySearch.node.ts new file mode 100644 index 0000000..1a492ad --- /dev/null +++ b/src/nodes/Tavily/TavilySearch.node.ts @@ -0,0 +1,291 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + INodeExecutionData, + INodeType, + INodeTypeDescription, + INodeProperties, + NodeApiError, +} from 'n8n-workflow'; + +import { tavilyApiRequest } from './tavilyApi.utils'; + +// NEW: maintain a constant array for valid time ranges +const ALLOWED_TIME_RANGES = ['', 'day', 'week', 'month', 'year', 'd', 'w', 'm', 'y']; + +export class TavilySearch implements INodeType { + description: INodeTypeDescription = { + displayName: 'Tavily Search', + name: 'tavilySearch', + icon: 'file:tavily.svg', + group: ['transform'], + version: 1, + description: 'Execute a search query using the Tavily Search endpoint', + defaults: { + name: 'Tavily Search', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'tavilyApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + required: true, + description: 'The search query to execute with Tavily', + }, + { + displayName: 'Topic', + name: 'topic', + type: 'options', + options: [ + { name: 'General', value: 'general' }, + { name: 'News', value: 'news' }, + ], + default: 'general', + description: 'The category of the search', + }, + { + displayName: 'Search Depth', + name: 'searchDepth', + type: 'options', + options: [ + { name: 'Basic', value: 'basic' }, + { name: 'Advanced', value: 'advanced' }, + ], + default: 'basic', + description: 'Depth of the search (basic=1 credit, advanced=2 credits)', + }, + { + displayName: 'Max Results', + name: 'maxResults', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 20, + }, + default: 5, + description: 'Maximum number of search results to return (0-20)', + }, + { + displayName: 'Time Range', + name: 'timeRange', + type: 'options', + options: [ + { name: 'None', value: '' }, + { name: 'Day', value: 'day' }, + { name: 'Week', value: 'week' }, + { name: 'Month', value: 'month' }, + { name: 'Year', value: 'year' }, + { name: 'd', value: 'd' }, + { name: 'w', value: 'w' }, + { name: 'm', value: 'm' }, + { name: 'y', value: 'y' }, + ], + default: '', + description: 'Time range filter (relative to current date)', + }, + { + displayName: 'Days (News Only)', + name: 'days', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 3, + description: 'Number of days back from the current date to include (for topic=news)', + displayOptions: { + show: { + topic: ['news'], + }, + }, + }, + { + displayName: 'Include Answer', + name: 'includeAnswer', + type: 'options', + options: [ + { name: 'No', value: 'false' }, + { name: 'Basic', value: 'basic' }, + { name: 'Advanced', value: 'advanced' }, + ], + default: 'false', + description: 'Include an LLM-generated answer (basic or advanced)', + }, + { + displayName: 'Include Raw Content', + name: 'includeRawContent', + type: 'boolean', + default: false, + description: 'Include cleaned and parsed HTML content of each search result', + }, + { + displayName: 'Include Images', + name: 'includeImages', + type: 'boolean', + default: false, + description: 'Perform an image search and include images in the response', + }, + { + displayName: 'Include Image Descriptions', + name: 'includeImageDescriptions', + type: 'boolean', + displayOptions: { + show: { + includeImages: [true], + }, + }, + default: false, + description: 'When including images, also add a descriptive text for each image', + }, + { + displayName: 'Include Domains', + name: 'includeDomains', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Domain', + }, + default: [], + description: 'Domains to specifically include in the search results', + }, + { + displayName: 'Exclude Domains', + name: 'excludeDomains', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Domain', + }, + default: [], + description: 'Domains to specifically exclude from the search results', + }, + { + displayName: 'API Documentation', + name: 'apiDocumentationNotice', + type: 'notice', + default: '', + description: `### Tavily Search API + +**Endpoint**: POST /search +**Body Parameters**: +- query (required) +- topic (general|news) +- search_depth (basic|advanced) +- max_results (0-20) +- time_range (day|week|month|year|d|w|m|y) +- days (>=0, news only) +- include_answer (false|basic|advanced) +- include_raw_content (boolean) +- include_images (boolean) +- include_image_descriptions (boolean) +- include_domains (string[]) +- exclude_domains (string[]) + +**Response**: +- query +- answer (optional) +- images (optional) +- results +- response_time +`, + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const credentials = await this.getCredentials('tavilyApi'); + if (!credentials?.apiKey) { + // Standardize NodeApiError usage + throw new NodeApiError(this.getNode(), new Error('Missing Tavily API key in credentials.')); + } + const apiKey = credentials.apiKey as string; + + for (let i = 0; i < items.length; i++) { + try { + const query = this.getNodeParameter('query', i) as string; + const topic = this.getNodeParameter('topic', i) as string; + const searchDepth = this.getNodeParameter('searchDepth', i) as string; + const maxResults = this.getNodeParameter('maxResults', i) as number; + const timeRange = this.getNodeParameter('timeRange', i) as string; + const days = this.getNodeParameter('days', i) as number; + const includeAnswer = this.getNodeParameter('includeAnswer', i) as string; + const includeRawContent = this.getNodeParameter('includeRawContent', i) as boolean; + const includeImages = this.getNodeParameter('includeImages', i) as boolean; + const includeImageDescriptions = this.getNodeParameter('includeImageDescriptions', i) as boolean; + const includeDomains = this.getNodeParameter('includeDomains', i, []) as string[]; + const excludeDomains = this.getNodeParameter('excludeDomains', i, []) as string[]; + + // --- Validate input parameters --- + if (!query?.trim()) { + throw new NodeApiError(this.getNode(), new Error('Query parameter cannot be empty.')); + } + if (maxResults < 0 || maxResults > 20) { + throw new NodeApiError(this.getNode(), new Error('Max Results must be between 0 and 20.')); + } + // Use ALLOWED_TIME_RANGES constant + if (!ALLOWED_TIME_RANGES.includes(timeRange)) { + throw new NodeApiError(this.getNode(), new Error(`Invalid timeRange value. Allowed: ${ALLOWED_TIME_RANGES.join(', ')}`)); + } + if (topic === 'news' && days < 0) { + throw new NodeApiError(this.getNode(), new Error('Days must be >= 0 when topic is "news".')); + } + // ---------------------------------- + + const body: Record = { + query, + topic, + search_depth: searchDepth, + max_results: maxResults, + include_raw_content: includeRawContent, + include_images: includeImages, + include_image_descriptions: includeImageDescriptions, + }; + + if (timeRange) { + body.time_range = timeRange; + } + if (topic === 'news') { + body.days = days; + } + if (includeAnswer !== 'false') { + body.include_answer = includeAnswer; + } + if (Array.isArray(includeDomains) && includeDomains.length > 0) { + body.include_domains = includeDomains; + } + if (Array.isArray(excludeDomains) && excludeDomains.length > 0) { + body.exclude_domains = excludeDomains; + } + + // Make request via the shared utility + const result = await tavilyApiRequest.call( + this, + 'POST', + '/search', + body, + apiKey, + ); + + returnData.push({ json: result }); + } catch (error) { + // Ensure consistent error wrapping + if (error instanceof NodeApiError) { + throw error; + } + throw new NodeApiError(this.getNode(), error as Error); + } + } + + return [returnData]; + } +} diff --git a/src/nodes/Tavily/tavilyApi.utils.ts b/src/nodes/Tavily/tavilyApi.utils.ts new file mode 100644 index 0000000..af54352 --- /dev/null +++ b/src/nodes/Tavily/tavilyApi.utils.ts @@ -0,0 +1,85 @@ +import axios, { AxiosError, AxiosResponse } from 'axios'; +import { NodeApiError } from 'n8n-workflow'; +import { IExecuteFunctions } from 'n8n-core'; + +/** + * Common function to call the Tavily API. + * Handles request execution and standard error handling. + */ +export async function tavilyApiRequest( + this: IExecuteFunctions, + method: 'POST', + resource: '/search' | '/extract', + body: Record, + apiKey: string, +): Promise { + + let response: AxiosResponse; + + try { + response = await axios.request({ + method, + url: `https://api.tavily.com${resource}`, + data: body, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + }); + } catch (error) { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + let message = `Tavily API error: ${error.message}`; + if (status) { + switch (status) { + case 400: + message = 'Tavily error [400]: Bad Request.'; + break; + case 401: + message = 'Tavily error [401]: Unauthorized.'; + break; + case 403: + message = 'Tavily error [403]: Forbidden.'; + break; + case 429: + message = 'Tavily error [429]: Too many requests.'; + break; + case 500: + message = 'Tavily error [500]: Internal Server Error.'; + break; + default: + message = `Tavily API error [${status}]: ${error.message}`; + break; + } + } + + // If the Tavily API returned a body with error details, append them + if (error.response?.data) { + const data = error.response.data; + const additionalMsgParts: string[] = []; + + if (data.error_code) { + additionalMsgParts.push(`error_code: ${data.error_code}`); + } + if (data.error_message) { + additionalMsgParts.push(`error_message: ${data.error_message}`); + } + if (!data.error_code && !data.error_message) { + // Fall back to a full JSON dump if no known fields + additionalMsgParts.push(JSON.stringify(data)); + } + + // Append to main message + if (additionalMsgParts.length > 0) { + message += ` | ${additionalMsgParts.join(' | ')}`; + } + } + + throw new NodeApiError(this.getNode(), new Error(message)); + } else { + throw new NodeApiError(this.getNode(), error as Error); + } + } + + return response.data; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..163a60d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "module": "CommonJS", + "rootDir": "src", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +}