n8n-nodes-plex-datasource
Version:
n8n node for calling Plex ERP Data Sources
704 lines • 30.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlexDataSource = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const axios_1 = __importDefault(require("axios"));
class PlexDataSource {
description = {
displayName: 'Plex ERP Data Source',
name: 'plexDataSource',
icon: 'file:plex.svg',
group: ['ai'],
version: 1,
subtitle: 'Execute Plex Data Source: {{$parameter["dataSourceId"]}}',
description: 'Execute Plex ERP Data Source queries via REST API',
defaults: {
name: 'Plex Data Source',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'plexBasicAuthApi',
required: true,
},
],
properties: [
// Basic Configuration Section
{
displayName: 'Description',
name: 'description',
type: 'string',
typeOptions: {
rows: 4,
},
default: '',
placeholder: 'Describe what this data source call does...',
description: 'Free text area to describe the node\'s purpose and functionality',
},
{
displayName: 'Environment',
name: 'environment',
type: 'options',
options: [
{
name: 'Production',
value: 'production',
},
{
name: 'Test',
value: 'test',
},
],
default: 'production',
required: true,
description: 'Select between production and test environments',
},
{
displayName: 'Colo Name',
name: 'coloName',
type: 'string',
default: '',
placeholder: 'Enter colo name (leave blank for cloud)',
description: 'Enter the colo name for on-premise installations. Leave blank for cloud-based instances.',
},
{
displayName: 'Data Source ID',
name: 'dataSourceId',
type: 'string',
default: '',
required: true,
placeholder: '12345',
description: 'The unique identifier for the Plex data source to execute',
},
// URL Preview
{
displayName: 'Generated URL Preview',
name: 'urlPreview',
type: 'options',
default: '',
required: false,
typeOptions: {
loadOptionsMethod: 'getUrlPreview',
},
description: 'This shows the URL that will be called based on your current settings',
},
// Body Configuration Section
{
displayName: 'Body Input Method',
name: 'bodyInputMethod',
type: 'options',
options: [
{
name: 'Key-Value Form',
value: 'form',
},
{
name: 'Raw JSON',
value: 'json',
},
],
default: 'form',
required: true,
description: 'How to configure the request body',
},
// Key-Value Form Parameters
{
displayName: 'Body Parameters',
name: 'bodyParameters',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
sortable: true,
},
default: {},
options: [
{
name: 'parameter',
displayName: 'Parameter',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
required: true,
description: 'Parameter name',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Parameter value',
},
{
displayName: 'Data Type',
name: 'dataType',
type: 'options',
options: [
{
name: 'String',
value: 'string',
},
{
name: 'Number',
value: 'number',
},
{
name: 'Boolean',
value: 'boolean',
},
{
name: 'Date',
value: 'date',
},
],
default: 'string',
description: 'The data type for this parameter',
},
{
displayName: 'Skip if Null',
name: 'skipIfNull',
type: 'boolean',
default: false,
description: 'Skip this parameter if incoming data is null',
},
{
displayName: 'Data Format',
name: 'format',
type: 'string',
default: '',
placeholder: 'e.g., YYYY-MM-DD for dates',
description: 'Format specification for data conversion',
displayOptions: {
show: {
dataType: ['date'],
},
},
},
],
},
],
displayOptions: {
show: {
bodyInputMethod: ['form'],
},
},
description: 'Configure key-value pairs for the request body',
},
// Raw JSON Input
{
displayName: 'JSON Body',
name: 'jsonBody',
type: 'json',
default: '{}',
description: 'Raw JSON body for the request',
displayOptions: {
show: {
bodyInputMethod: ['json'],
},
},
},
// Advanced Settings Section
{
displayName: 'Advanced Settings',
name: 'advancedSettings',
type: 'collection',
placeholder: 'Add Setting',
default: {},
options: [
{
displayName: 'Retry on Failure',
name: 'retryOnFailure',
type: 'boolean',
default: false,
description: 'Enable automatic retry on failed requests',
},
{
displayName: 'Max Retry Attempts',
name: 'maxRetryAttempts',
type: 'number',
default: 3,
typeOptions: {
minValue: 1,
maxValue: 10,
},
displayOptions: {
show: {
retryOnFailure: [true],
},
},
description: 'Maximum number of retry attempts',
},
{
displayName: 'Retry Wait Time (ms)',
name: 'retryWaitTime',
type: 'number',
default: 1000,
typeOptions: {
minValue: 100,
maxValue: 30000,
},
displayOptions: {
show: {
retryOnFailure: [true],
},
},
description: 'Time to wait between retry attempts in milliseconds',
},
{
displayName: 'Request Timeout (ms)',
name: 'timeout',
type: 'number',
default: 30000,
typeOptions: {
minValue: 1000,
maxValue: 300000,
},
description: 'Request timeout in milliseconds',
},
{
displayName: 'Follow Redirects',
name: 'followRedirects',
type: 'boolean',
default: true,
description: 'Automatically follow HTTP redirects',
},
],
},
],
};
methods = {
loadOptions: {
async getUrlPreview() {
try {
const environment = this.getCurrentNodeParameter('environment');
const coloName = this.getCurrentNodeParameter('coloName');
const dataSourceId = this.getCurrentNodeParameter('dataSourceId');
if (!dataSourceId) {
return [{ name: 'Enter Data Source ID to see URL preview', value: '' }];
}
const url = generateUrl(environment || 'production', coloName || '', dataSourceId);
return [{ name: url, value: url }];
}
catch (error) {
return [{ name: 'Error generating URL preview', value: '' }];
}
},
},
};
async execute() {
const items = this.getInputData();
const returnData = [];
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
// Get parameters
const environment = this.getNodeParameter('environment', itemIndex);
const coloName = this.getNodeParameter('coloName', itemIndex);
const dataSourceId = this.getNodeParameter('dataSourceId', itemIndex);
const bodyInputMethod = this.getNodeParameter('bodyInputMethod', itemIndex);
const advancedSettings = this.getNodeParameter('advancedSettings', itemIndex, {});
// Validate required parameters
if (!environment) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Environment is required');
}
if (!dataSourceId || dataSourceId.trim() === '') {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Data Source ID is required');
}
if (!bodyInputMethod) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Body Input Method is required');
}
// Get credentials
const credentials = await this.getCredentials('plexBasicAuthApi');
if (!credentials) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No credentials configured. Please add Plex Basic Auth credentials.');
}
if (!credentials.username || !credentials.password) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid credentials. Username and password are required.');
}
// Generate URL
const url = generateUrl(environment, coloName, dataSourceId);
// Prepare request body
const requestBody = await prepareRequestBody(this, bodyInputMethod, itemIndex);
// Build request config using Axios's built-in auth option for Basic Auth
const requestConfig = {
method: 'POST',
url,
data: requestBody,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
auth: {
username: String(credentials.username),
password: String(credentials.password),
},
timeout: advancedSettings.timeout || 30000,
maxRedirects: advancedSettings.followRedirects !== false ? 5 : 0,
maxBodyLength: Infinity,
};
// Deep logging for n8n output: full request/response
const redactedRequestConfig = {
...requestConfig,
headers: {
...requestConfig.headers,
Authorization: '[REDACTED]'
},
// Remove auth property from log for security
auth: undefined
};
let response;
let responseData;
try {
response = await executeRequest(requestConfig, advancedSettings);
responseData = processResponse(response, url);
// Always attach request/response log
responseData.json.__plexFullLog = {
requestConfig: redactedRequestConfig,
response: response ? {
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data
} : undefined
};
returnData.push(responseData);
}
catch (err) {
// Type guard for error object
const e = err;
const errorLog = {
error: true,
errorMessage: e.message,
errorStack: e.stack,
errorCode: e.code,
errorResponse: e.response ? {
status: e.response.status,
statusText: e.response.statusText,
headers: e.response.headers,
data: e.response.data
} : undefined,
requestConfig: redactedRequestConfig
};
if (this.continueOnFail()) {
returnData.push({
json: {
...errorLog,
__plexFullLog: errorLog
},
pairedItem: itemIndex
});
}
else {
// Attach log to error object for n8n error display
e.__plexFullLog = errorLog;
throw err;
}
}
// Skip the old returnData.push(responseData); now handled above
}
catch (error) {
console.error('PlexDataSource Error Details:', {
message: error.message,
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
code: error.code,
stack: error.stack
});
let errorMessage = error.message || 'Unknown error occurred';
let errorDetails = '';
// Get the current parameters for better error reporting
let dataSourceId = '';
let url = '';
try {
dataSourceId = this.getNodeParameter('dataSourceId', itemIndex);
const environment = this.getNodeParameter('environment', itemIndex);
const coloName = this.getNodeParameter('coloName', itemIndex);
url = generateUrl(environment, coloName, dataSourceId);
}
catch (paramError) {
console.error('Error getting parameters for error reporting:', paramError);
// Ignore parameter access errors during error handling
}
// Provide more specific error messages based on error type
if (error.response) {
// HTTP error response from server
const status = error.response.status;
const statusText = error.response.statusText;
const responseData = error.response.data;
// Check if the response contains a specific error message
if (responseData) {
if (typeof responseData === 'string') {
errorDetails = responseData;
}
else if (responseData.message) {
errorDetails = responseData.message;
}
else if (responseData.error) {
errorDetails = responseData.error;
}
else if (responseData.errors && Array.isArray(responseData.errors)) {
errorDetails = responseData.errors.join(', ');
}
else {
errorDetails = JSON.stringify(responseData);
}
}
switch (status) {
case 400:
errorMessage = 'Bad Request: The request parameters are invalid';
if (!errorDetails) {
errorDetails = `Check your Data Source ID (${dataSourceId}) and request body parameters`;
}
break;
case 401:
errorMessage = 'Authentication Failed: Invalid credentials';
if (!errorDetails) {
errorDetails = 'Please verify your username and password in the credential settings';
}
break;
case 403:
errorMessage = 'Access Forbidden: Insufficient permissions';
if (!errorDetails) {
errorDetails = 'Your account may not have access to this data source';
}
break;
case 404:
errorMessage = 'Not Found: Data source or endpoint does not exist';
if (!errorDetails) {
errorDetails = `Data Source ID "${dataSourceId}" may not exist or the URL "${url}" is incorrect`;
}
break;
case 419:
errorMessage = 'Authentication Timeout: Session expired or timed out';
if (!errorDetails) {
errorDetails = 'Your session has expired or timed out. Please re-authenticate or check your credentials/server session settings.';
}
break;
case 500:
errorMessage = 'Server Error: Plex API internal error';
if (!errorDetails) {
errorDetails = 'The Plex server encountered an internal error. Try again later.';
}
break;
default:
errorMessage = `HTTP ${status}: ${statusText}`;
if (!errorDetails) {
errorDetails = 'No additional details available';
}
}
}
else if (error.code === 'ECONNREFUSED') {
errorMessage = 'Connection Refused: Unable to connect to Plex server';
errorDetails = url ? `Check if the URL "${url}" is correct and the server is accessible` : 'Check your URL configuration';
}
else if (error.code === 'ENOTFOUND') {
errorMessage = 'DNS Error: Hostname not found';
errorDetails = url ? `Check if the hostname in "${url}" is correct` : 'Check your hostname configuration';
}
else if (error.code === 'ETIMEDOUT') {
errorMessage = 'Timeout: Request took too long to complete';
errorDetails = 'Consider increasing the timeout setting or check server availability';
}
const fullErrorMessage = errorDetails ? `${errorMessage}. ${errorDetails}` : errorMessage;
// Always attach __plexFullLog to error output for n8n visibility
const errorLog = {
error: true,
errorMessage,
details: errorDetails,
url,
dataSourceId,
statusCode: error.response?.status,
originalError: error.message,
__plexFullLog: {
error: true,
errorMessage: error.message,
errorStack: error.stack,
errorCode: error.code,
errorResponse: error.response ? {
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data
} : undefined,
requestConfig: undefined // Not available in this catch, but could be passed in future
}
};
if (this.continueOnFail()) {
returnData.push({
json: errorLog,
pairedItem: itemIndex
});
}
else {
// Attach __plexFullLog to error object for n8n error display
error.__plexFullLog = errorLog.__plexFullLog;
const nodeError = new n8n_workflow_1.NodeApiError(this.getNode(), error, {
message: fullErrorMessage,
description: errorDetails,
httpCode: error.response?.status?.toString(),
});
// Attach __plexFullLog to the thrown error for n8n UI
nodeError.__plexFullLog = errorLog.__plexFullLog;
throw nodeError;
}
}
}
return [returnData];
}
}
exports.PlexDataSource = PlexDataSource;
/**
* Generate the API URL based on environment and colo settings
*/
function generateUrl(environment, coloName, dataSourceId) {
let baseUrl;
if (coloName && coloName.trim() !== '') {
// On-premise deployment
if (environment === 'test') {
baseUrl = `https://${coloName}.test.on.plex.com`;
}
else {
baseUrl = `https://${coloName}.on.plex.com`;
}
}
else {
// Cloud deployment
if (environment === 'test') {
baseUrl = 'https://test.cloud.plex.com';
}
else {
baseUrl = 'https://cloud.plex.com';
}
}
return `${baseUrl}/api/datasources/${dataSourceId}/execute?format=2`;
}
/**
* Prepare the request body based on input method
*/
async function prepareRequestBody(context, bodyInputMethod, itemIndex) {
if (bodyInputMethod === 'json') {
const jsonBody = context.getNodeParameter('jsonBody', itemIndex);
try {
return JSON.parse(jsonBody);
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Invalid JSON in body: ${error.message}`);
}
}
else {
// Form-based key-value pairs
const bodyParameters = context.getNodeParameter('bodyParameters.parameter', itemIndex, []);
const body = {};
for (const param of bodyParameters) {
let value = param.value;
// Skip if null and skipIfNull is enabled
if (param.skipIfNull && (value === null || value === undefined || value === '')) {
continue;
}
// Apply data type conversion
value = convertDataType(context, value, param.dataType, param.format);
body[param.key] = value;
}
return body;
}
}
/**
* Convert value to specified data type
*/
function convertDataType(context, value, dataType, format) {
if (value === null || value === undefined) {
return value;
}
switch (dataType) {
case 'number': {
const num = Number(value);
if (isNaN(num)) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Cannot convert "${value}" to number`);
}
return num;
}
case 'boolean': {
if (typeof value === 'boolean') {
return value;
}
const str = String(value).toLowerCase();
return str === 'true' || str === '1' || str === 'yes';
}
case 'date':
if (format) {
// Use custom format if provided
return formatDate(context, value, format);
}
return new Date(value).toISOString();
case 'string':
default:
return String(value);
}
}
/**
* Format date according to specified format
*/
function formatDate(context, value, format) {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Invalid date value: ${value}`);
}
// Simple format replacement - could be enhanced with a proper date formatting library
return format
.replace('YYYY', date.getFullYear().toString())
.replace('MM', (date.getMonth() + 1).toString().padStart(2, '0'))
.replace('DD', date.getDate().toString().padStart(2, '0'))
.replace('HH', date.getHours().toString().padStart(2, '0'))
.replace('mm', date.getMinutes().toString().padStart(2, '0'))
.replace('ss', date.getSeconds().toString().padStart(2, '0'));
}
/**
* Execute HTTP request with retry logic
*/
async function executeRequest(config, advancedSettings) {
const maxAttempts = advancedSettings.retryOnFailure ? (advancedSettings.maxRetryAttempts || 3) : 1;
const waitTime = advancedSettings.retryWaitTime || 1000;
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const startTime = Date.now();
const response = await (0, axios_1.default)(config);
const endTime = Date.now();
// Add execution time to response
response.executionTime = endTime - startTime;
return response;
}
catch (error) {
lastError = error;
// Don't retry on final attempt
if (attempt === maxAttempts) {
break;
}
// Wait before retry with exponential backoff
const delay = waitTime * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
/**
* Process the HTTP response
*/
function processResponse(response, requestUrl) {
return {
json: {
statusCode: response.status,
headers: response.headers,
body: response.data,
requestUrl,
executionTime: response.executionTime || 0,
},
};
}
//# sourceMappingURL=PlexDataSource.node.js.map