mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(Airtop Node): Add File operations and scroll micro-interaction (#15089)
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
import { NodeApiError, type IExecuteFunctions, type INode } from 'n8n-workflow';
|
||||
import { NodeApiError, type IExecuteFunctions, type INode, type IDataObject } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { SESSION_MODE } from './actions/common/fields';
|
||||
import type { TScrollingMode } from './constants';
|
||||
import {
|
||||
ERROR_MESSAGES,
|
||||
DEFAULT_TIMEOUT_MINUTES,
|
||||
MIN_TIMEOUT_MINUTES,
|
||||
MAX_TIMEOUT_MINUTES,
|
||||
INTEGRATION_URL,
|
||||
SESSION_STATUS,
|
||||
OPERATION_TIMEOUT,
|
||||
} from './constants';
|
||||
import { apiRequest } from './transport';
|
||||
import type { IAirtopResponse } from './transport/types';
|
||||
import type { IAirtopResponse, IAirtopSessionResponse } from './transport/types';
|
||||
|
||||
/**
|
||||
* Validate a required string field
|
||||
@@ -25,7 +27,7 @@ export function validateRequiredStringField(
|
||||
field: string,
|
||||
fieldName: string,
|
||||
) {
|
||||
let value = this.getNodeParameter(field, index) as string;
|
||||
let value = this.getNodeParameter(field, index, '') as string;
|
||||
value = (value || '').trim();
|
||||
const errorMessage = ERROR_MESSAGES.REQUIRED_PARAMETER.replace('{{field}}', fieldName);
|
||||
|
||||
@@ -156,34 +158,113 @@ export function validateUrl(this: IExecuteFunctions, index: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the Proxy URL parameter
|
||||
* Validate the Proxy configuration
|
||||
* @param this - The execution context
|
||||
* @param index - The index of the node
|
||||
* @param proxy - The value of the Proxy parameter
|
||||
* @returns The validated proxy URL
|
||||
* @returns The validated proxy configuration
|
||||
*/
|
||||
export function validateProxyUrl(this: IExecuteFunctions, index: number, proxy: string) {
|
||||
let proxyUrl = this.getNodeParameter('proxyUrl', index, '') as string;
|
||||
proxyUrl = (proxyUrl || '').trim();
|
||||
export function validateProxy(this: IExecuteFunctions, index: number) {
|
||||
const proxyParam = this.getNodeParameter('proxy', index, '') as
|
||||
| 'none'
|
||||
| 'integrated'
|
||||
| 'proxyUrl';
|
||||
const proxyConfig = this.getNodeParameter('proxyConfig', index, '') as {
|
||||
country: string;
|
||||
sticky: boolean;
|
||||
};
|
||||
const isConfigEmpty = Object.keys(proxyConfig).length === 0;
|
||||
|
||||
// only validate proxyUrl if proxy is custom
|
||||
if (proxy !== 'custom') {
|
||||
return '';
|
||||
if (proxyParam === 'integrated') {
|
||||
return {
|
||||
proxy: isConfigEmpty ? true : { ...proxyConfig },
|
||||
};
|
||||
}
|
||||
|
||||
if (!proxyUrl) {
|
||||
throw new NodeOperationError(this.getNode(), ERROR_MESSAGES.PROXY_URL_REQUIRED, {
|
||||
// handle custom proxy configuration
|
||||
if (proxyParam === 'proxyUrl') {
|
||||
return {
|
||||
proxy: validateRequiredStringField.call(this, index, 'proxyUrl', 'Proxy URL'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
proxy: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the scrollBy amount parameter
|
||||
* @param this - The execution context
|
||||
* @param index - The index of the node
|
||||
* @param parameterName - The name of the parameter
|
||||
* @returns The validated scrollBy amount
|
||||
*/
|
||||
export function validateScrollByAmount(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
parameterName: string,
|
||||
) {
|
||||
const regex = /^(?:-?\d{1,3}(?:%|px))$/;
|
||||
const scrollBy = this.getNodeParameter(parameterName, index, {}) as {
|
||||
xAxis?: string;
|
||||
yAxis?: string;
|
||||
};
|
||||
|
||||
if (!scrollBy?.xAxis && !scrollBy?.yAxis) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const allAxisValid = [scrollBy.xAxis, scrollBy.yAxis]
|
||||
.filter(Boolean)
|
||||
.every((axis) => regex.test(axis ?? ''));
|
||||
|
||||
if (!allAxisValid) {
|
||||
throw new NodeOperationError(this.getNode(), ERROR_MESSAGES.SCROLL_BY_AMOUNT_INVALID, {
|
||||
itemIndex: index,
|
||||
});
|
||||
}
|
||||
|
||||
if (!proxyUrl.startsWith('http')) {
|
||||
throw new NodeOperationError(this.getNode(), ERROR_MESSAGES.PROXY_URL_INVALID, {
|
||||
return scrollBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the scroll mode parameter
|
||||
* @param this - The execution context
|
||||
* @param index - The index of the node
|
||||
* @returns Scroll mode
|
||||
* @throws Error if the scroll mode or scroll parameters are invalid
|
||||
*/
|
||||
export function validateScrollingMode(this: IExecuteFunctions, index: number): TScrollingMode {
|
||||
const scrollingMode = this.getNodeParameter(
|
||||
'scrollingMode',
|
||||
index,
|
||||
'automatic',
|
||||
) as TScrollingMode;
|
||||
|
||||
const scrollToEdge = this.getNodeParameter('scrollToEdge.edgeValues', index, {}) as {
|
||||
xAxis?: string;
|
||||
yAxis?: string;
|
||||
};
|
||||
const scrollBy = this.getNodeParameter('scrollBy.scrollValues', index, {}) as {
|
||||
xAxis?: string;
|
||||
yAxis?: string;
|
||||
};
|
||||
|
||||
if (scrollingMode !== 'manual') {
|
||||
return scrollingMode;
|
||||
}
|
||||
|
||||
// validate manual scroll parameters
|
||||
const emptyScrollBy = !scrollBy.xAxis && !scrollBy.yAxis;
|
||||
const emptyScrollToEdge = !scrollToEdge.xAxis && !scrollToEdge.yAxis;
|
||||
|
||||
if (emptyScrollBy && emptyScrollToEdge) {
|
||||
throw new NodeOperationError(this.getNode(), ERROR_MESSAGES.SCROLL_MODE_INVALID, {
|
||||
itemIndex: index,
|
||||
});
|
||||
}
|
||||
|
||||
return proxyUrl;
|
||||
return scrollingMode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,6 +354,56 @@ export function shouldCreateNewSession(this: IExecuteFunctions, index: number) {
|
||||
return Boolean(sessionMode && sessionMode === SESSION_MODE.NEW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session and wait until the session is ready
|
||||
* @param this - The execution context
|
||||
* @param parameters - The parameters for the session
|
||||
* @returns The session ID
|
||||
*/
|
||||
export async function createSession(
|
||||
this: IExecuteFunctions,
|
||||
parameters: IDataObject,
|
||||
timeout = OPERATION_TIMEOUT,
|
||||
): Promise<{ sessionId: string }> {
|
||||
// Request session creation
|
||||
const response = (await apiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'/sessions',
|
||||
parameters,
|
||||
)) as IAirtopSessionResponse;
|
||||
const sessionId = response?.data?.id;
|
||||
|
||||
if (!sessionId) {
|
||||
throw new NodeApiError(this.getNode(), {
|
||||
message: 'Failed to create session',
|
||||
code: 500,
|
||||
});
|
||||
}
|
||||
|
||||
// Poll until the session is ready or timeout is reached
|
||||
let sessionStatus = response?.data?.status;
|
||||
const startTime = Date.now();
|
||||
|
||||
while (sessionStatus !== SESSION_STATUS.RUNNING) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
throw new NodeApiError(this.getNode(), {
|
||||
message: ERROR_MESSAGES.TIMEOUT_REACHED,
|
||||
code: 500,
|
||||
});
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const sessionStatusResponse = (await apiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/sessions/${sessionId}`,
|
||||
)) as IAirtopSessionResponse;
|
||||
sessionStatus = sessionStatusResponse.data.status;
|
||||
}
|
||||
|
||||
return { sessionId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session and window
|
||||
* @param this - The execution context
|
||||
@@ -284,11 +415,10 @@ export async function createSessionAndWindow(
|
||||
index: number,
|
||||
): Promise<{ sessionId: string; windowId: string }> {
|
||||
const node = this.getNode();
|
||||
const noCodeEndpoint = `${INTEGRATION_URL}/create-session`;
|
||||
const profileName = validateProfileName.call(this, index);
|
||||
const url = validateRequiredStringField.call(this, index, 'url', 'URL');
|
||||
|
||||
const { sessionId } = await apiRequest.call(this, 'POST', noCodeEndpoint, {
|
||||
const { sessionId } = await createSession.call(this, {
|
||||
configuration: {
|
||||
profileName,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user