n8n-nodes-base
Version:
Base nodes of n8n
553 lines • 21.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Wait = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const validation_1 = require("./validation");
const utilities_1 = require("../../utils/utilities");
const common_descriptions_1 = require("../Form/common.descriptions");
const utils_1 = require("../Form/utils/utils");
const description_1 = require("../Webhook/description");
const Webhook_node_1 = require("../Webhook/Webhook.node");
const toWaitAmount = {
displayName: 'Wait Amount',
name: 'amount',
type: 'number',
typeOptions: {
minValue: 0,
numberPrecision: 2,
},
default: 1,
description: 'The time to wait',
validateType: 'number',
};
const unitSelector = {
displayName: 'Wait Unit',
name: 'unit',
type: 'options',
options: [
{
name: 'Seconds',
value: 'seconds',
},
{
name: 'Minutes',
value: 'minutes',
},
{
name: 'Hours',
value: 'hours',
},
{
name: 'Days',
value: 'days',
},
],
default: 'hours',
description: 'The time unit of the Wait Amount value',
};
const waitTimeProperties = [
{
displayName: 'Limit Wait Time',
name: 'limitWaitTime',
type: 'boolean',
default: false,
description: 'Whether to limit the time this node should wait for a user response before execution resumes',
displayOptions: {
show: {
resume: ['webhook', 'form'],
},
},
},
{
displayName: 'Limit Type',
name: 'limitType',
type: 'options',
default: 'afterTimeInterval',
description: 'Sets the condition for the execution to resume. Can be a specified date or after some time.',
displayOptions: {
show: {
limitWaitTime: [true],
resume: ['webhook', 'form'],
},
},
options: [
{
name: 'After Time Interval',
description: 'Waits for a certain amount of time',
value: 'afterTimeInterval',
},
{
name: 'At Specified Time',
description: 'Waits until the set date and time to continue',
value: 'atSpecifiedTime',
},
],
},
{
displayName: 'Amount',
name: 'resumeAmount',
type: 'number',
displayOptions: {
show: {
limitType: ['afterTimeInterval'],
limitWaitTime: [true],
resume: ['webhook', 'form'],
},
},
typeOptions: {
minValue: 0,
numberPrecision: 2,
},
default: 1,
description: 'The time to wait',
},
{
displayName: 'Unit',
name: 'resumeUnit',
type: 'options',
displayOptions: {
show: {
limitType: ['afterTimeInterval'],
limitWaitTime: [true],
resume: ['webhook', 'form'],
},
},
options: [
{
name: 'Seconds',
value: 'seconds',
},
{
name: 'Minutes',
value: 'minutes',
},
{
name: 'Hours',
value: 'hours',
},
{
name: 'Days',
value: 'days',
},
],
default: 'hours',
description: 'Unit of the interval value',
},
{
displayName: 'Max Date and Time',
name: 'maxDateAndTime',
type: 'dateTime',
displayOptions: {
show: {
limitType: ['atSpecifiedTime'],
limitWaitTime: [true],
resume: ['webhook', 'form'],
},
},
default: '',
description: 'Continue execution after the specified date and time',
},
];
const webhookSuffix = {
displayName: 'Webhook Suffix',
name: 'webhookSuffix',
type: 'string',
default: '',
placeholder: 'webhook',
noDataExpression: true,
description: 'This suffix path will be appended to the restart URL. Helpful when using multiple wait nodes.',
};
const displayOnWebhook = {
show: {
resume: ['webhook'],
},
};
const displayOnFormSubmission = {
show: {
resume: ['form'],
},
};
const onFormSubmitProperties = (0, utilities_1.updateDisplayOptions)(displayOnFormSubmission, [
common_descriptions_1.formTitle,
common_descriptions_1.formDescription,
common_descriptions_1.formFields,
common_descriptions_1.formRespondMode,
]);
const onWebhookCallProperties = (0, utilities_1.updateDisplayOptions)(displayOnWebhook, [
{
...description_1.httpMethodsProperty,
description: 'The HTTP method of the Webhook call',
},
description_1.responseCodeProperty,
description_1.responseModeProperty,
description_1.responseDataProperty,
description_1.responseBinaryPropertyNameProperty,
]);
const webhookPath = '={{$parameter["options"]["webhookSuffix"] || ""}}';
const waitingTooltip = (parameters, resumeUrl, formResumeUrl) => {
const resume = parameters.resume;
if (['webhook', 'form'].includes(resume)) {
const { webhookSuffix } = (parameters.options ?? {});
const suffix = webhookSuffix && typeof webhookSuffix !== 'object' ? `/${webhookSuffix}` : '';
let message = '';
const baseUrl = resume === 'form' ? formResumeUrl : resumeUrl;
// Insert suffix before query parameters if present (for URLs with ?signature=token)
// Note: Cannot use URL class here because it is not available in expressions
let url;
const queryIndex = baseUrl.indexOf('?');
if (queryIndex !== -1) {
url = baseUrl.slice(0, queryIndex) + suffix + baseUrl.slice(queryIndex);
}
else {
url = baseUrl + suffix;
}
if (resume === 'form') {
message = 'Execution will continue when form is submitted on ';
}
if (resume === 'webhook') {
message = 'Execution will continue when webhook is received on ';
}
return `${message}<a href="${url}" target="_blank">${url}</a>`;
}
return 'Execution will continue when wait time is over';
};
class Wait extends Webhook_node_1.Webhook {
authPropertyName = 'incomingAuthentication';
description = {
displayName: 'Wait',
name: 'wait',
icon: 'fa:pause-circle',
iconColor: 'crimson',
group: ['organization'],
version: [1, 1.1],
description: 'Wait before continue with execution',
defaults: {
name: 'Wait',
color: '#804050',
},
inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
credentials: (0, description_1.credentialsProperty)(this.authPropertyName),
waitingNodeTooltip: `={{ (${waitingTooltip})($parameter, $execution.resumeUrl, $execution.resumeFormUrl) }}`,
webhooks: [
{
...description_1.defaultWebhookDescription,
responseData: '={{$parameter["responseData"]}}',
path: webhookPath,
restartWebhook: true,
},
{
name: 'default',
httpMethod: 'GET',
responseMode: 'onReceived',
path: webhookPath,
restartWebhook: true,
isFullPath: true,
nodeType: 'form',
},
{
name: 'default',
httpMethod: 'POST',
responseMode: '={{$parameter["responseMode"]}}',
responseData: '={{$parameter["responseMode"] === "lastNode" ? "noData" : undefined}}',
path: webhookPath,
restartWebhook: true,
isFullPath: true,
nodeType: 'form',
},
],
properties: [
{
displayName: 'Resume',
name: 'resume',
type: 'options',
builderHint: {
message: 'For user approval workflows, consider using nodes with operation: "sendAndWait" (e.g., email, Slack) instead of Wait node. If using "webhook", the URL will be generated at runtime and can be referenced with {{ $execution.resumeUrl }}.',
},
options: [
{
name: 'After Time Interval',
value: 'timeInterval',
description: 'Waits for a certain amount of time',
},
{
name: 'At Specified Time',
value: 'specificTime',
description: 'Waits until a specific date and time to continue',
},
{
name: 'On Webhook Call',
value: 'webhook',
description: 'Waits for a webhook call before continuing',
},
{
name: 'On Form Submitted',
value: 'form',
description: 'Waits for a form submission before continuing',
},
],
default: 'timeInterval',
description: 'Determines the waiting mode to use before the workflow continues',
},
{
displayName: 'Authentication',
name: 'incomingAuthentication',
type: 'options',
options: [
{
name: 'Basic Auth',
value: 'basicAuth',
},
{
name: 'None',
value: 'none',
},
],
default: 'none',
description: 'If and how incoming resume-webhook-requests to $execution.resumeFormUrl should be authenticated for additional security',
displayOptions: {
show: {
resume: ['form'],
},
},
},
{
...(0, description_1.authenticationProperty)(this.authPropertyName),
description: 'If and how incoming resume-webhook-requests to $execution.resumeUrl should be authenticated for additional security',
displayOptions: displayOnWebhook,
},
// ----------------------------------
// resume:specificTime
// ----------------------------------
{
displayName: 'Date and Time',
name: 'dateTime',
type: 'dateTime',
displayOptions: {
show: {
resume: ['specificTime'],
},
},
default: '',
description: 'The date and time to wait for before continuing',
required: true,
},
// ----------------------------------
// resume:timeInterval
// ----------------------------------
{
...toWaitAmount,
displayOptions: {
show: {
resume: ['timeInterval'],
'@version': [1],
},
},
},
{
...toWaitAmount,
default: 5,
displayOptions: {
show: {
resume: ['timeInterval'],
},
hide: {
'@version': [1],
},
},
},
{
...unitSelector,
displayOptions: {
show: {
resume: ['timeInterval'],
'@version': [1],
},
},
},
{
...unitSelector,
default: 'seconds',
displayOptions: {
show: {
resume: ['timeInterval'],
},
hide: {
'@version': [1],
},
},
},
// ----------------------------------
// resume:webhook & form
// ----------------------------------
{
displayName: 'The webhook URL will be generated at run time. It can be referenced with the <strong>$execution.resumeUrl</strong> variable. Send it somewhere before getting to this node. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.wait" target="_blank">More info</a>',
name: 'webhookNotice',
type: 'notice',
displayOptions: displayOnWebhook,
default: '',
},
{
displayName: 'The form url will be generated at run time. It can be referenced with the <strong>$execution.resumeFormUrl</strong> variable. Send it somewhere before getting to this node. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.wait" target="_blank">More info</a>',
name: 'formNotice',
type: 'notice',
displayOptions: displayOnFormSubmission,
default: '',
},
...onFormSubmitProperties,
...onWebhookCallProperties,
...waitTimeProperties,
{
...description_1.optionsProperty,
displayOptions: displayOnWebhook,
options: [...description_1.optionsProperty.options, webhookSuffix],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
displayOptions: {
show: {
resume: ['form'],
},
hide: {
responseMode: ['responseNode'],
},
},
options: [common_descriptions_1.appendAttributionToForm, common_descriptions_1.respondWithOptions, webhookSuffix],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
displayOptions: {
show: {
resume: ['form'],
},
hide: {
responseMode: ['onReceived', 'lastNode'],
},
},
options: [common_descriptions_1.appendAttributionToForm, webhookSuffix],
},
],
};
async webhook(context) {
const resume = context.getNodeParameter('resume', 0);
if (resume === 'form')
return await (0, utils_1.formWebhook)(context, this.authPropertyName);
return await super.webhook(context);
}
async execute(context) {
const resume = context.getNodeParameter('resume', 0);
if (['webhook', 'form'].includes(resume)) {
let hasFormTrigger = false;
if (resume === 'form') {
// Add signed resumeFormUrl to metadata for frontend to use when opening form popup
const resumeFormUrl = context.evaluateExpression('{{ $execution.resumeFormUrl }}', 0);
context.setMetadata({ resumeFormUrl });
const parentNodes = context.getParentNodes(context.getNode().name);
hasFormTrigger = parentNodes.some((node) => node.type === n8n_workflow_1.FORM_TRIGGER_NODE_TYPE);
}
if (resume === 'webhook') {
// Add signed resumeUrl to metadata for frontend to use in waiting tooltip
const resumeUrl = context.evaluateExpression('{{ $execution.resumeUrl }}', 0);
context.setMetadata({ resumeUrl });
}
const returnData = await this.configureAndPutToWait(context);
if (resume === 'form' && hasFormTrigger) {
context.sendResponse({
headers: {
location: context.evaluateExpression('{{ $execution.resumeFormUrl }}', 0),
},
statusCode: 307,
});
}
return returnData;
}
let waitTill;
if (resume === 'timeInterval') {
const unit = context.getNodeParameter('unit', 0);
if (!(0, validation_1.validateWaitUnit)(unit)) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), "Invalid wait unit. Valid units are 'seconds', 'minutes', 'hours', or 'days'.");
}
let waitAmount = context.getNodeParameter('amount', 0);
if (!(0, validation_1.validateWaitAmount)(waitAmount)) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Invalid wait amount. Please enter a number that is 0 or greater.');
}
if (unit === 'minutes') {
waitAmount *= 60;
}
if (unit === 'hours') {
waitAmount *= 60 * 60;
}
if (unit === 'days') {
waitAmount *= 60 * 60 * 24;
}
waitAmount *= 1000;
// Timezone does not change relative dates, since they are just
// a number of seconds added to the current timestamp
waitTill = new Date(new Date().getTime() + waitAmount);
}
else {
try {
const dateTimeStrRaw = context.getNodeParameter('dateTime', 0);
const parsedDateTime = (0, n8n_workflow_1.tryToParseDateTime)(dateTimeStrRaw, context.getTimezone());
waitTill = parsedDateTime.toUTC().toJSDate();
}
catch (e) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Cannot put execution to wait because `dateTime` parameter is not a valid date. Please pick a specific date and time to wait until.');
}
}
const waitValue = Math.max(waitTill.getTime() - new Date().getTime(), 0);
if (waitValue < 65000) {
// If wait time is shorter than 65 seconds leave execution active because
// we just check the database every 60 seconds.
return await new Promise((resolve, _reject) => {
const timer = setTimeout(() => resolve([context.getInputData()]), waitValue);
context.onExecutionCancellation(() => {
clearTimeout(timer);
resolve([context.getInputData()]);
});
});
}
// If longer than 65 seconds put execution to wait
return await this.putToWait(context, waitTill);
}
async configureAndPutToWait(context) {
let waitTill = n8n_workflow_1.WAIT_INDEFINITELY;
const limitWaitTime = context.getNodeParameter('limitWaitTime', 0);
if (limitWaitTime === true) {
const limitType = context.getNodeParameter('limitType', 0);
if (limitType === 'afterTimeInterval') {
let waitAmount = context.getNodeParameter('resumeAmount', 0);
const resumeUnit = context.getNodeParameter('resumeUnit', 0);
if (resumeUnit === 'minutes') {
waitAmount *= 60;
}
if (resumeUnit === 'hours') {
waitAmount *= 60 * 60;
}
if (resumeUnit === 'days') {
waitAmount *= 60 * 60 * 24;
}
waitAmount *= 1000;
waitTill = new Date(new Date().getTime() + waitAmount);
}
else {
waitTill = new Date(context.getNodeParameter('maxDateAndTime', 0));
}
}
return await this.putToWait(context, waitTill);
}
async putToWait(context, waitTill) {
await context.putExecutionToWait(waitTill);
return [context.getInputData()];
}
}
exports.Wait = Wait;
//# sourceMappingURL=Wait.node.js.map