mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(Google Calendar Node): Updates and fixes (#10715)
Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
This commit is contained in:
@@ -84,6 +84,7 @@ export const calendarFields: INodeProperties[] = [
|
|||||||
show: {
|
show: {
|
||||||
operation: ['availability'],
|
operation: ['availability'],
|
||||||
resource: ['calendar'],
|
resource: ['calendar'],
|
||||||
|
'@version': [{ _cnd: { lt: 1.3 } }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
@@ -98,11 +99,44 @@ export const calendarFields: INodeProperties[] = [
|
|||||||
show: {
|
show: {
|
||||||
operation: ['availability'],
|
operation: ['availability'],
|
||||||
resource: ['calendar'],
|
resource: ['calendar'],
|
||||||
|
'@version': [{ _cnd: { lt: 1.3 } }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
description: 'End of the interval',
|
description: 'End of the interval',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Start Time',
|
||||||
|
name: 'timeMin',
|
||||||
|
type: 'dateTime',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['availability'],
|
||||||
|
resource: ['calendar'],
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '={{ $now }}',
|
||||||
|
description:
|
||||||
|
'Start of the interval, use <a href="https://docs.n8n.io/code/cookbook/luxon/" target="_blank">expression</a> to set a date, or switch to fixed mode to choose date from widget',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'End Time',
|
||||||
|
name: 'timeMax',
|
||||||
|
type: 'dateTime',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['availability'],
|
||||||
|
resource: ['calendar'],
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: "={{ $now.plus(1, 'hour') }}",
|
||||||
|
description:
|
||||||
|
'End of the interval, use <a href="https://docs.n8n.io/code/cookbook/luxon/" target="_blank">expression</a> to set a date, or switch to fixed mode to choose date from widget',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Options',
|
displayName: 'Options',
|
||||||
name: 'options',
|
name: 'options',
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const eventFields: INodeProperties[] = [
|
|||||||
show: {
|
show: {
|
||||||
operation: ['create'],
|
operation: ['create'],
|
||||||
resource: ['event'],
|
resource: ['event'],
|
||||||
|
'@version': [{ _cnd: { lt: 1.3 } }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
@@ -126,11 +127,44 @@ export const eventFields: INodeProperties[] = [
|
|||||||
show: {
|
show: {
|
||||||
operation: ['create'],
|
operation: ['create'],
|
||||||
resource: ['event'],
|
resource: ['event'],
|
||||||
|
'@version': [{ _cnd: { lt: 1.3 } }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
description: 'End time of the event',
|
description: 'End time of the event',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Start',
|
||||||
|
name: 'start',
|
||||||
|
type: 'dateTime',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['create'],
|
||||||
|
resource: ['event'],
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '={{ $now }}',
|
||||||
|
description:
|
||||||
|
'Start time of the event, use <a href="https://docs.n8n.io/code/cookbook/luxon/" target="_blank">expression</a> to set a date, or switch to fixed mode to choose date from widget',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'End',
|
||||||
|
name: 'end',
|
||||||
|
type: 'dateTime',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['create'],
|
||||||
|
resource: ['event'],
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: "={{ $now.plus(1, 'hour') }}",
|
||||||
|
description:
|
||||||
|
'End time of the event, use <a href="https://docs.n8n.io/code/cookbook/luxon/" target="_blank">expression</a> to set a date, or switch to fixed mode to choose date from widget',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Use Default Reminders',
|
displayName: 'Use Default Reminders',
|
||||||
name: 'useDefaultReminders',
|
name: 'useDefaultReminders',
|
||||||
@@ -553,6 +587,19 @@ export const eventFields: INodeProperties[] = [
|
|||||||
description:
|
description:
|
||||||
'The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned.',
|
'The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return Next Instance of Recurring Event',
|
||||||
|
name: 'returnNextInstance',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether to return the next instance of a recurring event instead of the event itself',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Timezone',
|
displayName: 'Timezone',
|
||||||
name: 'timeZone',
|
name: 'timeZone',
|
||||||
@@ -629,6 +676,36 @@ export const eventFields: INodeProperties[] = [
|
|||||||
default: 50,
|
default: 50,
|
||||||
description: 'Max number of results to return',
|
description: 'Max number of results to return',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'After',
|
||||||
|
name: 'timeMin',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '={{ $now }}',
|
||||||
|
description:
|
||||||
|
'At least some part of the event must be after this time, use <a href="https://docs.n8n.io/code/cookbook/luxon/" target="_blank">expression</a> to set a date, or switch to fixed mode to choose date from widget',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
operation: ['getAll'],
|
||||||
|
resource: ['event'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Before',
|
||||||
|
name: 'timeMax',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '={{ $now.plus({ week: 1 }) }}',
|
||||||
|
description:
|
||||||
|
'At least some part of the event must be before this time, use <a href="https://docs.n8n.io/code/cookbook/luxon/" target="_blank">expression</a> to set a date, or switch to fixed mode to choose date from widget',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
operation: ['getAll'],
|
||||||
|
resource: ['event'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Options',
|
displayName: 'Options',
|
||||||
name: 'options',
|
name: 'options',
|
||||||
@@ -647,14 +724,39 @@ export const eventFields: INodeProperties[] = [
|
|||||||
name: 'timeMin',
|
name: 'timeMin',
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
default: '',
|
default: '',
|
||||||
description: 'At least some part of the event must be after this time',
|
description:
|
||||||
|
'At least some part of the event must be after this time, use <a href="https://docs.n8n.io/code/cookbook/luxon/" target="_blank">expression</a> to set a date, or switch to fixed mode to choose date from widget',
|
||||||
|
displayOptions: {
|
||||||
|
hide: {
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Before',
|
displayName: 'Before',
|
||||||
name: 'timeMax',
|
name: 'timeMax',
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
default: '',
|
default: '',
|
||||||
description: 'At least some part of the event must be before this time',
|
description:
|
||||||
|
'At least some part of the event must be before this time, use <a href="https://docs.n8n.io/code/cookbook/luxon/" target="_blank">expression</a> to set a date, or switch to fixed mode to choose date from widget',
|
||||||
|
displayOptions: {
|
||||||
|
hide: {
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Expand Events',
|
||||||
|
name: 'singleEvents',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether to expand recurring events into instances and only return single one-off events and instances of recurring events, but not the underlying recurring events themselves',
|
||||||
|
displayOptions: {
|
||||||
|
hide: {
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Fields',
|
displayName: 'Fields',
|
||||||
@@ -708,6 +810,34 @@ export const eventFields: INodeProperties[] = [
|
|||||||
description:
|
description:
|
||||||
'Free text search terms to find events that match these terms in any field, except for extended properties',
|
'Free text search terms to find events that match these terms in any field, except for extended properties',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Recurring Event Handling',
|
||||||
|
name: 'recurringEventHandling',
|
||||||
|
type: 'options',
|
||||||
|
default: 'expand',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'All Occurrences',
|
||||||
|
value: 'expand',
|
||||||
|
description: 'Return all instances of recurring event for specified time range',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'First Occurrence',
|
||||||
|
value: 'first',
|
||||||
|
description: 'Return event with specified recurrence rule',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Next Occurrence',
|
||||||
|
value: 'next',
|
||||||
|
description: 'Return next instance of recurring event',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Show Deleted',
|
displayName: 'Show Deleted',
|
||||||
name: 'showDeleted',
|
name: 'showDeleted',
|
||||||
@@ -723,14 +853,7 @@ export const eventFields: INodeProperties[] = [
|
|||||||
default: false,
|
default: false,
|
||||||
description: 'Whether to include hidden invitations in the result',
|
description: 'Whether to include hidden invitations in the result',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
displayName: 'Single Events',
|
|
||||||
name: 'singleEvents',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
description:
|
|
||||||
'Whether to expand recurring events into instances and only return single one-off events and instances of recurring events, but not the underlying recurring events themselves',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
displayName: 'Timezone',
|
displayName: 'Timezone',
|
||||||
name: 'timeZone',
|
name: 'timeZone',
|
||||||
@@ -797,6 +920,30 @@ export const eventFields: INodeProperties[] = [
|
|||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Modify',
|
||||||
|
name: 'modifyTarget',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Recurring Event Instance',
|
||||||
|
value: 'instance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Recurring Event',
|
||||||
|
value: 'event',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'instance',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||||
|
resource: ['event'],
|
||||||
|
operation: ['update'],
|
||||||
|
eventId: [{ _cnd: { includes: '_' } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Use Default Reminders',
|
displayName: 'Use Default Reminders',
|
||||||
name: 'useDefaultReminders',
|
name: 'useDefaultReminders',
|
||||||
|
|||||||
@@ -34,3 +34,8 @@ export interface IEvent {
|
|||||||
visibility?: string;
|
visibility?: string;
|
||||||
conferenceData?: IConferenceData;
|
conferenceData?: IConferenceData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RecurringEventInstance = {
|
||||||
|
recurringEventId?: string;
|
||||||
|
start: { dateTime: string; date: string };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
IHttpRequestMethods,
|
IHttpRequestMethods,
|
||||||
ILoadOptionsFunctions,
|
ILoadOptionsFunctions,
|
||||||
|
INode,
|
||||||
INodeListSearchItems,
|
INodeListSearchItems,
|
||||||
INodeListSearchResult,
|
INodeListSearchResult,
|
||||||
IPollFunctions,
|
IPollFunctions,
|
||||||
IRequestOptions,
|
IRequestOptions,
|
||||||
JsonObject,
|
JsonObject,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeApiError } from 'n8n-workflow';
|
import { NodeApiError, NodeOperationError, sleep } from 'n8n-workflow';
|
||||||
import { RRule } from 'rrule';
|
import { RRule } from 'rrule';
|
||||||
|
|
||||||
|
import type { RecurringEventInstance } from './EventInterface';
|
||||||
|
|
||||||
export async function googleApiRequest(
|
export async function googleApiRequest(
|
||||||
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
||||||
method: IHttpRequestMethods,
|
method: IHttpRequestMethods,
|
||||||
@@ -50,7 +54,6 @@ export async function googleApiRequestAllItems(
|
|||||||
propertyName: string,
|
propertyName: string,
|
||||||
method: IHttpRequestMethods,
|
method: IHttpRequestMethods,
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
|
|
||||||
body: any = {},
|
body: any = {},
|
||||||
query: IDataObject = {},
|
query: IDataObject = {},
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
@@ -127,58 +130,75 @@ export async function getTimezones(
|
|||||||
return { results };
|
return { results };
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecurentEvent = {
|
export type RecurrentEvent = {
|
||||||
start: {
|
start: {
|
||||||
dateTime: string;
|
date?: string;
|
||||||
timeZone: string;
|
dateTime?: string;
|
||||||
|
timeZone?: string;
|
||||||
};
|
};
|
||||||
end: {
|
end: {
|
||||||
dateTime: string;
|
date?: string;
|
||||||
timeZone: string;
|
dateTime?: string;
|
||||||
|
timeZone?: string;
|
||||||
};
|
};
|
||||||
recurrence: string[];
|
recurrence: string[];
|
||||||
nextOccurrence?: {
|
nextOccurrence?: {
|
||||||
start: {
|
start: {
|
||||||
dateTime: string;
|
dateTime: string;
|
||||||
timeZone: string;
|
timeZone?: string;
|
||||||
};
|
};
|
||||||
end: {
|
end: {
|
||||||
dateTime: string;
|
dateTime: string;
|
||||||
timeZone: string;
|
timeZone?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function addNextOccurrence(items: RecurentEvent[]) {
|
export function addNextOccurrence(items: RecurrentEvent[]) {
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.recurrence) {
|
if (item.recurrence) {
|
||||||
let eventRecurrence;
|
let eventRecurrence;
|
||||||
try {
|
try {
|
||||||
eventRecurrence = item.recurrence.find((r) => r.toUpperCase().startsWith('RRULE'));
|
eventRecurrence = item.recurrence.find((r) => r.toUpperCase().startsWith('RRULE'));
|
||||||
|
|
||||||
if (!eventRecurrence) continue;
|
if (!eventRecurrence) continue;
|
||||||
|
|
||||||
const rrule = RRule.fromString(eventRecurrence);
|
const start = moment(item.start.dateTime || item.end.date).utc();
|
||||||
|
const end = moment(item.end.dateTime || item.end.date).utc();
|
||||||
|
|
||||||
|
const rruleWithStartDate = `DTSTART:${start.format(
|
||||||
|
'YYYYMMDDTHHmmss',
|
||||||
|
)}Z\n${eventRecurrence}`;
|
||||||
|
|
||||||
|
const rrule = RRule.fromString(rruleWithStartDate);
|
||||||
|
|
||||||
const until = rrule.options?.until;
|
const until = rrule.options?.until;
|
||||||
|
|
||||||
const now = new Date();
|
const now = moment().utc();
|
||||||
if (until && until < now) {
|
|
||||||
|
if (until && moment(until).isBefore(now)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextOccurrence = rrule.after(new Date());
|
const nextDate = rrule.after(now.toDate(), false);
|
||||||
|
|
||||||
|
if (nextDate) {
|
||||||
|
const nextStart = moment(nextDate);
|
||||||
|
|
||||||
|
const duration = moment.duration(moment(end).diff(moment(start)));
|
||||||
|
const nextEnd = moment(nextStart).add(duration);
|
||||||
|
|
||||||
item.nextOccurrence = {
|
item.nextOccurrence = {
|
||||||
start: {
|
start: {
|
||||||
dateTime: moment(nextOccurrence).format(),
|
dateTime: nextStart.format(),
|
||||||
timeZone: item.start.timeZone,
|
timeZone: item.start.timeZone,
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
dateTime: moment(nextOccurrence)
|
dateTime: nextEnd.format(),
|
||||||
.add(moment(item.end.dateTime).diff(moment(item.start.dateTime)))
|
|
||||||
.format(),
|
|
||||||
timeZone: item.end.timeZone,
|
timeZone: item.end.timeZone,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Error adding next occurrence ${eventRecurrence}`);
|
console.log(`Error adding next occurrence ${eventRecurrence}`);
|
||||||
}
|
}
|
||||||
@@ -193,3 +213,92 @@ export function addTimezoneToDate(date: string, timezone: string) {
|
|||||||
if (hasTimezone(date)) return date;
|
if (hasTimezone(date)) return date;
|
||||||
return moment.tz(date, timezone).utc().format();
|
return moment.tz(date, timezone).utc().format();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function requestWithRetries(
|
||||||
|
node: INode,
|
||||||
|
requestFn: () => Promise<any>,
|
||||||
|
retryCount: number = 0,
|
||||||
|
maxRetries: number = 10,
|
||||||
|
itemIndex: number = 0,
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
return await requestFn();
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof NodeApiError)) {
|
||||||
|
throw new NodeOperationError(node, error.message, { itemIndex });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryCount >= maxRetries) throw error;
|
||||||
|
|
||||||
|
if (error.httpCode === '403' || error.httpCode === '429') {
|
||||||
|
const delay = 1000 * Math.pow(2, retryCount);
|
||||||
|
|
||||||
|
console.log(`Rate limit hit. Retrying in ${delay}ms... (Attempt ${retryCount + 1})`);
|
||||||
|
|
||||||
|
await sleep(delay);
|
||||||
|
return await requestWithRetries(node, requestFn, retryCount + 1, maxRetries, itemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function googleApiRequestWithRetries({
|
||||||
|
context,
|
||||||
|
method,
|
||||||
|
resource,
|
||||||
|
body = {},
|
||||||
|
qs = {},
|
||||||
|
uri,
|
||||||
|
headers = {},
|
||||||
|
itemIndex = 0,
|
||||||
|
}: {
|
||||||
|
context: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions;
|
||||||
|
method: IHttpRequestMethods;
|
||||||
|
resource: string;
|
||||||
|
body?: any;
|
||||||
|
qs?: IDataObject;
|
||||||
|
uri?: string;
|
||||||
|
headers?: IDataObject;
|
||||||
|
itemIndex?: number;
|
||||||
|
}) {
|
||||||
|
const requestFn = async (): Promise<any> => {
|
||||||
|
return await googleApiRequest.call(context, method, resource, body, qs, uri, headers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const retryCount = 0;
|
||||||
|
const maxRetries = 10;
|
||||||
|
|
||||||
|
return await requestWithRetries(context.getNode(), requestFn, retryCount, maxRetries, itemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventExtendYearIntoFuture = (
|
||||||
|
data: RecurringEventInstance[],
|
||||||
|
timezone: string,
|
||||||
|
currentYear?: number, // for testing purposes
|
||||||
|
) => {
|
||||||
|
const thisYear = currentYear || moment().tz(timezone).year();
|
||||||
|
|
||||||
|
return data.some((event) => {
|
||||||
|
if (!event.recurringEventId) return false;
|
||||||
|
|
||||||
|
const eventStart = event.start.dateTime || event.start.date;
|
||||||
|
|
||||||
|
const eventDateTime = moment(eventStart).tz(timezone);
|
||||||
|
if (!eventDateTime.isValid()) return false;
|
||||||
|
|
||||||
|
const targetYear = eventDateTime.year();
|
||||||
|
|
||||||
|
if (targetYear - thisYear >= 1) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function dateObjectToISO<T>(date: T): string {
|
||||||
|
if (date instanceof DateTime) return date.toISO();
|
||||||
|
if (date instanceof Date) return date.toISOString();
|
||||||
|
return date as string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
|
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "5 workflow automations for Mattermost that we love at n8n",
|
"label": "5 workflow automation for Mattermost that we love at n8n",
|
||||||
"icon": "🤖",
|
"icon": "🤖",
|
||||||
"url": "https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/"
|
"url": "https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,22 +8,33 @@ import type {
|
|||||||
INodeType,
|
INodeType,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
JsonObject,
|
JsonObject,
|
||||||
|
NodeExecutionHint,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import {
|
||||||
|
NodeConnectionType,
|
||||||
|
NodeApiError,
|
||||||
|
NodeOperationError,
|
||||||
|
NodeExecutionOutput,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeApiError, NodeOperationError } from 'n8n-workflow';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { calendarFields, calendarOperations } from './CalendarDescription';
|
import { calendarFields, calendarOperations } from './CalendarDescription';
|
||||||
import { eventFields, eventOperations } from './EventDescription';
|
import { eventFields, eventOperations } from './EventDescription';
|
||||||
import type { IEvent } from './EventInterface';
|
import type { IEvent, RecurringEventInstance } from './EventInterface';
|
||||||
import {
|
import {
|
||||||
addNextOccurrence,
|
addNextOccurrence,
|
||||||
addTimezoneToDate,
|
addTimezoneToDate,
|
||||||
|
dateObjectToISO,
|
||||||
encodeURIComponentOnce,
|
encodeURIComponentOnce,
|
||||||
|
eventExtendYearIntoFuture,
|
||||||
getCalendars,
|
getCalendars,
|
||||||
getTimezones,
|
getTimezones,
|
||||||
googleApiRequest,
|
googleApiRequest,
|
||||||
googleApiRequestAllItems,
|
googleApiRequestAllItems,
|
||||||
|
googleApiRequestWithRetries,
|
||||||
|
type RecurrentEvent,
|
||||||
} from './GenericFunctions';
|
} from './GenericFunctions';
|
||||||
|
import { sortItemKeysByPriorityList } from '../../../utils/utilities';
|
||||||
|
|
||||||
export class GoogleCalendar implements INodeType {
|
export class GoogleCalendar implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
@@ -31,7 +42,7 @@ export class GoogleCalendar implements INodeType {
|
|||||||
name: 'googleCalendar',
|
name: 'googleCalendar',
|
||||||
icon: 'file:googleCalendar.svg',
|
icon: 'file:googleCalendar.svg',
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: [1, 1.1, 1.2],
|
version: [1, 1.1, 1.2, 1.3],
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume Google Calendar API',
|
description: 'Consume Google Calendar API',
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -132,6 +143,7 @@ export class GoogleCalendar implements INodeType {
|
|||||||
const returnData: INodeExecutionData[] = [];
|
const returnData: INodeExecutionData[] = [];
|
||||||
const length = items.length;
|
const length = items.length;
|
||||||
const qs: IDataObject = {};
|
const qs: IDataObject = {};
|
||||||
|
const hints: NodeExecutionHint[] = [];
|
||||||
let responseData;
|
let responseData;
|
||||||
|
|
||||||
const resource = this.getNodeParameter('resource', 0);
|
const resource = this.getNodeParameter('resource', 0);
|
||||||
@@ -148,8 +160,8 @@ export class GoogleCalendar implements INodeType {
|
|||||||
const calendarId = decodeURIComponent(
|
const calendarId = decodeURIComponent(
|
||||||
this.getNodeParameter('calendar', i, '', { extractValue: true }) as string,
|
this.getNodeParameter('calendar', i, '', { extractValue: true }) as string,
|
||||||
);
|
);
|
||||||
const timeMin = this.getNodeParameter('timeMin', i) as string;
|
const timeMin = dateObjectToISO(this.getNodeParameter('timeMin', i));
|
||||||
const timeMax = this.getNodeParameter('timeMax', i) as string;
|
const timeMax = dateObjectToISO(this.getNodeParameter('timeMax', i));
|
||||||
const options = this.getNodeParameter('options', i);
|
const options = this.getNodeParameter('options', i);
|
||||||
const outputFormat = options.outputFormat || 'availability';
|
const outputFormat = options.outputFormat || 'availability';
|
||||||
const tz = this.getNodeParameter('options.timezone', i, '', {
|
const tz = this.getNodeParameter('options.timezone', i, '', {
|
||||||
@@ -200,8 +212,8 @@ export class GoogleCalendar implements INodeType {
|
|||||||
const calendarId = encodeURIComponentOnce(
|
const calendarId = encodeURIComponentOnce(
|
||||||
this.getNodeParameter('calendar', i, '', { extractValue: true }) as string,
|
this.getNodeParameter('calendar', i, '', { extractValue: true }) as string,
|
||||||
);
|
);
|
||||||
const start = this.getNodeParameter('start', i) as string;
|
const start = dateObjectToISO(this.getNodeParameter('start', i));
|
||||||
const end = this.getNodeParameter('end', i) as string;
|
const end = dateObjectToISO(this.getNodeParameter('end', i));
|
||||||
const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean;
|
const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean;
|
||||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||||
|
|
||||||
@@ -379,16 +391,33 @@ export class GoogleCalendar implements INodeType {
|
|||||||
if (tz) {
|
if (tz) {
|
||||||
qs.timeZone = tz;
|
qs.timeZone = tz;
|
||||||
}
|
}
|
||||||
responseData = await googleApiRequest.call(
|
responseData = (await googleApiRequest.call(
|
||||||
this,
|
this,
|
||||||
'GET',
|
'GET',
|
||||||
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
||||||
{},
|
{},
|
||||||
qs,
|
qs,
|
||||||
);
|
)) as IDataObject;
|
||||||
|
|
||||||
if (responseData) {
|
if (responseData) {
|
||||||
responseData = addNextOccurrence([responseData]);
|
if (nodeVersion >= 1.3 && options.returnNextInstance && responseData.recurrence) {
|
||||||
|
const eventInstances =
|
||||||
|
((
|
||||||
|
(await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
`/calendar/v3/calendars/${calendarId}/events/${responseData.id}/instances`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
timeMin: new Date().toISOString(),
|
||||||
|
maxResults: 1,
|
||||||
|
},
|
||||||
|
)) as IDataObject
|
||||||
|
).items as IDataObject[]) || [];
|
||||||
|
responseData = eventInstances[0] ? [eventInstances[0]] : [responseData];
|
||||||
|
} else {
|
||||||
|
responseData = addNextOccurrence([responseData as RecurrentEvent]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//https://developers.google.com/calendar/v3/reference/events/list
|
//https://developers.google.com/calendar/v3/reference/events/list
|
||||||
@@ -401,6 +430,22 @@ export class GoogleCalendar implements INodeType {
|
|||||||
const tz = this.getNodeParameter('options.timeZone', i, '', {
|
const tz = this.getNodeParameter('options.timeZone', i, '', {
|
||||||
extractValue: true,
|
extractValue: true,
|
||||||
}) as string;
|
}) as string;
|
||||||
|
|
||||||
|
if (nodeVersion >= 1.3) {
|
||||||
|
const timeMin = dateObjectToISO(this.getNodeParameter('timeMin', i));
|
||||||
|
const timeMax = dateObjectToISO(this.getNodeParameter('timeMax', i));
|
||||||
|
if (timeMin) {
|
||||||
|
qs.timeMin = addTimezoneToDate(timeMin as string, tz || timezone);
|
||||||
|
}
|
||||||
|
if (timeMax) {
|
||||||
|
qs.timeMax = addTimezoneToDate(timeMax as string, tz || timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.recurringEventHandling || options.recurringEventHandling === 'expand') {
|
||||||
|
qs.singleEvents = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options.iCalUID) {
|
if (options.iCalUID) {
|
||||||
qs.iCalUID = options.iCalUID as string;
|
qs.iCalUID = options.iCalUID as string;
|
||||||
}
|
}
|
||||||
@@ -423,16 +468,19 @@ export class GoogleCalendar implements INodeType {
|
|||||||
qs.singleEvents = options.singleEvents as boolean;
|
qs.singleEvents = options.singleEvents as boolean;
|
||||||
}
|
}
|
||||||
if (options.timeMax) {
|
if (options.timeMax) {
|
||||||
qs.timeMax = addTimezoneToDate(options.timeMax as string, tz || timezone);
|
qs.timeMax = addTimezoneToDate(dateObjectToISO(options.timeMax), tz || timezone);
|
||||||
}
|
}
|
||||||
if (options.timeMin) {
|
if (options.timeMin) {
|
||||||
qs.timeMin = addTimezoneToDate(options.timeMin as string, tz || timezone);
|
qs.timeMin = addTimezoneToDate(dateObjectToISO(options.timeMin), tz || timezone);
|
||||||
}
|
}
|
||||||
if (tz) {
|
if (tz) {
|
||||||
qs.timeZone = tz;
|
qs.timeZone = tz;
|
||||||
}
|
}
|
||||||
if (options.updatedMin) {
|
if (options.updatedMin) {
|
||||||
qs.updatedMin = addTimezoneToDate(options.updatedMin as string, tz || timezone);
|
qs.updatedMin = addTimezoneToDate(
|
||||||
|
dateObjectToISO(options.updatedMin),
|
||||||
|
tz || timezone,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (options.fields) {
|
if (options.fields) {
|
||||||
qs.fields = options.fields as string;
|
qs.fields = options.fields as string;
|
||||||
@@ -460,15 +508,99 @@ export class GoogleCalendar implements INodeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (responseData) {
|
if (responseData) {
|
||||||
|
if (nodeVersion >= 1.3 && options.recurringEventHandling === 'next') {
|
||||||
|
const updatedEvents: IDataObject[] = [];
|
||||||
|
|
||||||
|
for (const event of responseData) {
|
||||||
|
if (event.recurrence) {
|
||||||
|
const eventInstances =
|
||||||
|
((
|
||||||
|
(await googleApiRequestWithRetries({
|
||||||
|
context: this,
|
||||||
|
method: 'GET',
|
||||||
|
resource: `/calendar/v3/calendars/${calendarId}/events/${event.id}/instances`,
|
||||||
|
qs: {
|
||||||
|
timeMin: new Date().toISOString(),
|
||||||
|
maxResults: 1,
|
||||||
|
},
|
||||||
|
itemIndex: i,
|
||||||
|
})) as IDataObject
|
||||||
|
).items as IDataObject[]) || [];
|
||||||
|
updatedEvents.push(eventInstances[0] || event);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedEvents.push(event);
|
||||||
|
}
|
||||||
|
responseData = updatedEvents;
|
||||||
|
} else if (nodeVersion >= 1.3 && options.recurringEventHandling === 'first') {
|
||||||
|
responseData = responseData.filter((event: IDataObject) => {
|
||||||
|
if (
|
||||||
|
qs.timeMin &&
|
||||||
|
event.recurrence &&
|
||||||
|
event.created &&
|
||||||
|
event.created < qs.timeMin
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
qs.timeMax &&
|
||||||
|
event.recurrence &&
|
||||||
|
event.created &&
|
||||||
|
event.created > qs.timeMax
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} else if (nodeVersion < 1.3) {
|
||||||
|
// in node version above or equal to 1.3, this would correspond to the 'expand' option,
|
||||||
|
// so no need to add the next occurrence as event instances returned by the API
|
||||||
responseData = addNextOccurrence(responseData);
|
responseData = addNextOccurrence(responseData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!qs.timeMax &&
|
||||||
|
(!options.recurringEventHandling || options.recurringEventHandling === 'expand')
|
||||||
|
) {
|
||||||
|
const suggestTrim = eventExtendYearIntoFuture(
|
||||||
|
responseData as RecurringEventInstance[],
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (suggestTrim) {
|
||||||
|
hints.push({
|
||||||
|
message:
|
||||||
|
"Some events repeat far into the future. To return less of them, add a 'Before' date or change the 'Recurring Event Handling' option.",
|
||||||
|
location: 'outputPane',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//https://developers.google.com/calendar/v3/reference/events/patch
|
//https://developers.google.com/calendar/v3/reference/events/patch
|
||||||
if (operation === 'update') {
|
if (operation === 'update') {
|
||||||
const calendarId = encodeURIComponentOnce(
|
const calendarId = encodeURIComponentOnce(
|
||||||
this.getNodeParameter('calendar', i, '', { extractValue: true }) as string,
|
this.getNodeParameter('calendar', i, '', { extractValue: true }) as string,
|
||||||
);
|
);
|
||||||
const eventId = this.getNodeParameter('eventId', i) as string;
|
let eventId = this.getNodeParameter('eventId', i) as string;
|
||||||
|
|
||||||
|
if (nodeVersion >= 1.3) {
|
||||||
|
const modifyTarget = this.getNodeParameter('modifyTarget', i, 'instance') as string;
|
||||||
|
if (modifyTarget === 'event') {
|
||||||
|
const instance = (await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
||||||
|
{},
|
||||||
|
qs,
|
||||||
|
)) as IDataObject;
|
||||||
|
eventId = instance.recurringEventId as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean;
|
const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean;
|
||||||
const updateFields = this.getNodeParameter('updateFields', i);
|
const updateFields = this.getNodeParameter('updateFields', i);
|
||||||
let updateTimezone = updateFields.timezone as string;
|
let updateTimezone = updateFields.timezone as string;
|
||||||
@@ -658,6 +790,30 @@ export class GoogleCalendar implements INodeType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [returnData];
|
|
||||||
|
const keysPriorityList = [
|
||||||
|
'id',
|
||||||
|
'summary',
|
||||||
|
'start',
|
||||||
|
'end',
|
||||||
|
'attendees',
|
||||||
|
'creator',
|
||||||
|
'organizer',
|
||||||
|
'description',
|
||||||
|
'location',
|
||||||
|
'created',
|
||||||
|
'updated',
|
||||||
|
];
|
||||||
|
|
||||||
|
let nodeExecutionData = returnData;
|
||||||
|
if (nodeVersion >= 1.3) {
|
||||||
|
nodeExecutionData = sortItemKeysByPriorityList(returnData, keysPriorityList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hints.length) {
|
||||||
|
return new NodeExecutionOutput([nodeExecutionData], hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [nodeExecutionData];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { addTimezoneToDate } from '../GenericFunctions';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
import type { RecurringEventInstance } from '../EventInterface';
|
||||||
|
import { addTimezoneToDate, dateObjectToISO, eventExtendYearIntoFuture } from '../GenericFunctions';
|
||||||
|
|
||||||
describe('addTimezoneToDate', () => {
|
describe('addTimezoneToDate', () => {
|
||||||
it('should add timezone to date', () => {
|
it('should add timezone to date', () => {
|
||||||
@@ -18,3 +21,87 @@ describe('addTimezoneToDate', () => {
|
|||||||
expect(result4).toBe('2021-09-01T12:00:00.000+08:00');
|
expect(result4).toBe('2021-09-01T12:00:00.000+08:00');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('dateObjectToISO', () => {
|
||||||
|
test('should return ISO string for DateTime instance', () => {
|
||||||
|
const mockDateTime = DateTime.fromISO('2025-01-07T12:00:00');
|
||||||
|
const result = dateObjectToISO(mockDateTime);
|
||||||
|
expect(result).toBe('2025-01-07T12:00:00.000+00:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return ISO string for Date instance', () => {
|
||||||
|
const mockDate = new Date('2025-01-07T12:00:00Z');
|
||||||
|
const result = dateObjectToISO(mockDate);
|
||||||
|
expect(result).toBe('2025-01-07T12:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return string when input is not a DateTime or Date instance', () => {
|
||||||
|
const inputString = '2025-01-07T12:00:00';
|
||||||
|
const result = dateObjectToISO(inputString);
|
||||||
|
expect(result).toBe(inputString);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('eventExtendYearIntoFuture', () => {
|
||||||
|
const timezone = 'UTC';
|
||||||
|
|
||||||
|
it('should return true if any event extends into the next year', () => {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
recurringEventId: '123',
|
||||||
|
start: { dateTime: '2026-01-01T00:00:00Z', date: null },
|
||||||
|
},
|
||||||
|
] as unknown as RecurringEventInstance[];
|
||||||
|
|
||||||
|
const result = eventExtendYearIntoFuture(events, timezone, 2025);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if no event extends into the next year', () => {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
recurringEventId: '123',
|
||||||
|
start: { dateTime: '2025-12-31T23:59:59Z', date: null },
|
||||||
|
},
|
||||||
|
] as unknown as RecurringEventInstance[];
|
||||||
|
|
||||||
|
const result = eventExtendYearIntoFuture(events, timezone, 2025);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for invalid event start dates', () => {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
recurringEventId: '123',
|
||||||
|
start: { dateTime: 'invalid-date', date: null },
|
||||||
|
},
|
||||||
|
] as unknown as RecurringEventInstance[];
|
||||||
|
|
||||||
|
const result = eventExtendYearIntoFuture(events, timezone, 2025);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for events without a recurringEventId', () => {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
recurringEventId: null,
|
||||||
|
start: { dateTime: '2025-01-01T00:00:00Z', date: null },
|
||||||
|
},
|
||||||
|
] as unknown as RecurringEventInstance[];
|
||||||
|
|
||||||
|
const result = eventExtendYearIntoFuture(events, timezone, 2025);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle events with only a date and no time', () => {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
recurringEventId: '123',
|
||||||
|
start: { dateTime: null, date: '2026-01-01' },
|
||||||
|
},
|
||||||
|
] as unknown as RecurringEventInstance[];
|
||||||
|
|
||||||
|
const result = eventExtendYearIntoFuture(events, timezone, 2025);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
import type { RecurrentEvent } from '../GenericFunctions';
|
||||||
|
import { addNextOccurrence } from '../GenericFunctions';
|
||||||
|
|
||||||
|
const mockNow = '2024-09-06T16:30:00+03:00';
|
||||||
|
jest.spyOn(global.Date, 'now').mockImplementation(() => moment(mockNow).valueOf());
|
||||||
|
|
||||||
|
describe('addNextOccurrence', () => {
|
||||||
|
it('should not modify event if no recurrence exists', () => {
|
||||||
|
const event: RecurrentEvent[] = [
|
||||||
|
{
|
||||||
|
start: {
|
||||||
|
dateTime: '2024-09-01T08:00:00Z',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: '2024-09-01T09:00:00Z',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
},
|
||||||
|
recurrence: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = addNextOccurrence(event);
|
||||||
|
|
||||||
|
expect(result[0].nextOccurrence).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle event with no RRULE correctly', () => {
|
||||||
|
const event: RecurrentEvent[] = [
|
||||||
|
{
|
||||||
|
start: {
|
||||||
|
dateTime: '2024-09-01T08:00:00Z',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: '2024-09-01T09:00:00Z',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
},
|
||||||
|
recurrence: ['FREQ=WEEKLY;COUNT=2'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = addNextOccurrence(event);
|
||||||
|
|
||||||
|
expect(result[0].nextOccurrence).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore recurrence if until date is in the past', () => {
|
||||||
|
const event: RecurrentEvent[] = [
|
||||||
|
{
|
||||||
|
start: {
|
||||||
|
dateTime: '2024-08-01T08:00:00Z',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: '2024-08-01T09:00:00Z',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
},
|
||||||
|
recurrence: ['RRULE:FREQ=DAILY;UNTIL=20240805T000000Z'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = addNextOccurrence(event);
|
||||||
|
|
||||||
|
expect(result[0].nextOccurrence).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully without breaking and return unchanged event', () => {
|
||||||
|
const event: RecurrentEvent[] = [
|
||||||
|
{
|
||||||
|
start: {
|
||||||
|
dateTime: '2024-09-06T17:30:00+03:00',
|
||||||
|
timeZone: 'Europe/Berlin',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: '2024-09-06T18:00:00+03:00',
|
||||||
|
timeZone: 'Europe/Berlin',
|
||||||
|
},
|
||||||
|
recurrence: ['xxxxx'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = addNextOccurrence(event);
|
||||||
|
|
||||||
|
expect(result).toEqual(event);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import type { MockProxy } from 'jest-mock-extended';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import type { INode, IExecuteFunctions, IDataObject, NodeExecutionOutput } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import * as genericFunctions from '../../GenericFunctions';
|
||||||
|
import { GoogleCalendar } from '../../GoogleCalendar.node';
|
||||||
|
|
||||||
|
let response: IDataObject[] | undefined = [];
|
||||||
|
let responseWithRetries: IDataObject | undefined = {};
|
||||||
|
|
||||||
|
jest.mock('../../GenericFunctions', () => {
|
||||||
|
const originalModule = jest.requireActual('../../GenericFunctions');
|
||||||
|
return {
|
||||||
|
...originalModule,
|
||||||
|
getTimezones: jest.fn(),
|
||||||
|
googleApiRequest: jest.fn(),
|
||||||
|
googleApiRequestAllItems: jest.fn(async function () {
|
||||||
|
return (() => response)();
|
||||||
|
}),
|
||||||
|
googleApiRequestWithRetries: jest.fn(async function () {
|
||||||
|
return (() => responseWithRetries)();
|
||||||
|
}),
|
||||||
|
addNextOccurrence: jest.fn(function (data: IDataObject[]) {
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Google Calendar Node', () => {
|
||||||
|
let googleCalendar: GoogleCalendar;
|
||||||
|
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
googleCalendar = new GoogleCalendar();
|
||||||
|
mockExecuteFunctions = mock<IExecuteFunctions>({
|
||||||
|
getInputData: jest.fn(),
|
||||||
|
getNode: jest.fn(),
|
||||||
|
getNodeParameter: jest.fn(),
|
||||||
|
getTimezone: jest.fn(),
|
||||||
|
helpers: {
|
||||||
|
constructExecutionMetaData: jest.fn().mockReturnValue([]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
response = undefined;
|
||||||
|
responseWithRetries = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Google Calendar > Event > Get Many', () => {
|
||||||
|
it('should configure get all request parameters in version 1.3', async () => {
|
||||||
|
// pre loop setup
|
||||||
|
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('event');
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('getAll');
|
||||||
|
mockExecuteFunctions.getTimezone.mockReturnValueOnce('Europe/Berlin');
|
||||||
|
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.3 }));
|
||||||
|
|
||||||
|
//operation setup
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(true); //returnAll
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('myCalendar'); //calendar
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({
|
||||||
|
iCalUID: 'uid',
|
||||||
|
maxAttendees: 25,
|
||||||
|
orderBy: 'startTime',
|
||||||
|
query: 'test query',
|
||||||
|
recurringEventHandling: 'expand',
|
||||||
|
showDeleted: true,
|
||||||
|
showHiddenInvitations: true,
|
||||||
|
updatedMin: '2024-12-21T00:00:00',
|
||||||
|
}); //options
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('Europe/Berlin'); //options.timeZone
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('2024-12-20T00:00:00'); //timeMin
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('2024-12-26T00:00:00'); //timeMax
|
||||||
|
|
||||||
|
await googleCalendar.execute.call(mockExecuteFunctions);
|
||||||
|
|
||||||
|
expect(genericFunctions.googleApiRequestAllItems).toHaveBeenCalledWith(
|
||||||
|
'items',
|
||||||
|
'GET',
|
||||||
|
'/calendar/v3/calendars/myCalendar/events',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
iCalUID: 'uid',
|
||||||
|
maxAttendees: 25,
|
||||||
|
orderBy: 'startTime',
|
||||||
|
q: 'test query',
|
||||||
|
showDeleted: true,
|
||||||
|
showHiddenInvitations: true,
|
||||||
|
singleEvents: true,
|
||||||
|
timeMax: '2024-12-25T23:00:00Z',
|
||||||
|
timeMin: '2024-12-19T23:00:00Z',
|
||||||
|
timeZone: 'Europe/Berlin',
|
||||||
|
updatedMin: '2024-12-20T23:00:00Z',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure get all recurringEventHandling equals next in version 1.3', async () => {
|
||||||
|
// pre loop setup
|
||||||
|
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('event');
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('getAll');
|
||||||
|
mockExecuteFunctions.getTimezone.mockReturnValueOnce('Europe/Berlin');
|
||||||
|
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.3 }));
|
||||||
|
|
||||||
|
//operation setup
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(true); //returnAll
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('myCalendar'); //calendar
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({
|
||||||
|
recurringEventHandling: 'next',
|
||||||
|
}); //options
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('Europe/Berlin'); //options.timeZone
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('2024-12-20T00:00:00'); //timeMin
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('2024-12-26T00:00:00'); //timeMax
|
||||||
|
|
||||||
|
response = [
|
||||||
|
{
|
||||||
|
recurrence: ['RRULE:FREQ=DAILY;COUNT=5'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
responseWithRetries = { items: [] };
|
||||||
|
|
||||||
|
const result = await googleCalendar.execute.call(mockExecuteFunctions);
|
||||||
|
|
||||||
|
expect(genericFunctions.googleApiRequestAllItems).toHaveBeenCalledWith(
|
||||||
|
'items',
|
||||||
|
'GET',
|
||||||
|
'/calendar/v3/calendars/myCalendar/events',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
timeMax: '2024-12-25T23:00:00Z',
|
||||||
|
timeMin: '2024-12-19T23:00:00Z',
|
||||||
|
timeZone: 'Europe/Berlin',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(genericFunctions.googleApiRequestWithRetries).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'GET',
|
||||||
|
itemIndex: 0,
|
||||||
|
resource: '/calendar/v3/calendars/myCalendar/events/undefined/instances',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([[]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure get all recurringEventHandling equals first in version 1.3', async () => {
|
||||||
|
// pre loop setup
|
||||||
|
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('event');
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('getAll');
|
||||||
|
mockExecuteFunctions.getTimezone.mockReturnValueOnce('Europe/Berlin');
|
||||||
|
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.3 }));
|
||||||
|
|
||||||
|
//operation setup
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(true); //returnAll
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('myCalendar'); //calendar
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({
|
||||||
|
recurringEventHandling: 'first',
|
||||||
|
}); //options
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('Europe/Berlin'); //options.timeZone
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('2024-12-20T00:00:00'); //timeMin
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('2024-12-26T00:00:00'); //timeMax
|
||||||
|
|
||||||
|
response = [
|
||||||
|
{
|
||||||
|
recurrence: ['RRULE:FREQ=DAILY;COUNT=5'],
|
||||||
|
created: '2024-12-19T00:00:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recurrence: ['RRULE:FREQ=DAILY;COUNT=5'],
|
||||||
|
created: '2024-12-27T00:00:00',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await googleCalendar.execute.call(mockExecuteFunctions);
|
||||||
|
|
||||||
|
expect(result).toEqual([[]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure get all should have hint in version 1.3', async () => {
|
||||||
|
// pre loop setup
|
||||||
|
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('event');
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('getAll');
|
||||||
|
mockExecuteFunctions.getTimezone.mockReturnValueOnce('Europe/Berlin');
|
||||||
|
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.3 }));
|
||||||
|
|
||||||
|
//operation setup
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(true); //returnAll
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('myCalendar'); //calendar
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); //options
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('Europe/Berlin'); //options.timeZone
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('2024-12-20T00:00:00'); //timeMin
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(''); //timeMax
|
||||||
|
|
||||||
|
response = [
|
||||||
|
{
|
||||||
|
recurrence: ['RRULE:FREQ=DAILY;COUNT=5'],
|
||||||
|
created: '2024-12-25T00:00:00',
|
||||||
|
recurringEventId: '1',
|
||||||
|
start: { dateTime: '2027-12-25T00:00:00' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await googleCalendar.execute.call(mockExecuteFunctions);
|
||||||
|
|
||||||
|
expect((result as NodeExecutionOutput).getHints()).toEqual([
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Some events repeat far into the future. To return less of them, add a 'Before' date or change the 'Recurring Event Handling' option.",
|
||||||
|
location: 'outputPane',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,7 +14,7 @@ jest.mock('../../GenericFunctions', () => ({
|
|||||||
encodeURIComponentOnce: jest.fn(),
|
encodeURIComponentOnce: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('RespondToWebhook Node', () => {
|
describe('Google Calendar Node', () => {
|
||||||
let googleCalendar: GoogleCalendar;
|
let googleCalendar: GoogleCalendar;
|
||||||
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { INodeExecutionData } from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
compareItems,
|
compareItems,
|
||||||
flattenKeys,
|
flattenKeys,
|
||||||
@@ -5,6 +7,7 @@ import {
|
|||||||
getResolvables,
|
getResolvables,
|
||||||
keysToLowercase,
|
keysToLowercase,
|
||||||
shuffleArray,
|
shuffleArray,
|
||||||
|
sortItemKeysByPriorityList,
|
||||||
wrapData,
|
wrapData,
|
||||||
} from '@utils/utilities';
|
} from '@utils/utilities';
|
||||||
|
|
||||||
@@ -252,3 +255,60 @@ describe('compareItems', () => {
|
|||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('sortItemKeysByPriorityList', () => {
|
||||||
|
it('should reorder keys based on priority list', () => {
|
||||||
|
const data: INodeExecutionData[] = [{ json: { c: 3, a: 1, b: 2 } }];
|
||||||
|
const priorityList = ['b', 'a'];
|
||||||
|
|
||||||
|
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||||
|
|
||||||
|
expect(Object.keys(result[0].json)).toEqual(['b', 'a', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort keys not in the priority list alphabetically', () => {
|
||||||
|
const data: INodeExecutionData[] = [{ json: { c: 3, a: 1, b: 2, d: 4 } }];
|
||||||
|
const priorityList = ['b', 'a'];
|
||||||
|
|
||||||
|
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||||
|
|
||||||
|
expect(Object.keys(result[0].json)).toEqual(['b', 'a', 'c', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort all keys alphabetically when priority list is empty', () => {
|
||||||
|
const data: INodeExecutionData[] = [{ json: { c: 3, a: 1, b: 2 } }];
|
||||||
|
const priorityList: string[] = [];
|
||||||
|
|
||||||
|
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||||
|
|
||||||
|
expect(Object.keys(result[0].json)).toEqual(['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle an empty data array', () => {
|
||||||
|
const data: INodeExecutionData[] = [];
|
||||||
|
const priorityList = ['b', 'a'];
|
||||||
|
|
||||||
|
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||||
|
|
||||||
|
// Expect an empty array since there is no data
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a single object in the data array', () => {
|
||||||
|
const data: INodeExecutionData[] = [{ json: { d: 4, b: 2, a: 1 } }];
|
||||||
|
const priorityList = ['a', 'b', 'c'];
|
||||||
|
|
||||||
|
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||||
|
|
||||||
|
expect(Object.keys(result[0].json)).toEqual(['a', 'b', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle duplicate keys in the priority list gracefully', () => {
|
||||||
|
const data: INodeExecutionData[] = [{ json: { d: 4, b: 2, a: 1 } }];
|
||||||
|
const priorityList = ['a', 'b', 'a'];
|
||||||
|
|
||||||
|
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||||
|
|
||||||
|
expect(Object.keys(result[0].json)).toEqual(['a', 'b', 'd']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -427,3 +427,37 @@ export function escapeHtml(text: string): string {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts each item json's keys by a priority list
|
||||||
|
*
|
||||||
|
* @param {INodeExecutionData[]} data The array of items which keys will be sorted
|
||||||
|
* @param {string[]} priorityList The priority list, keys of item.json will be sorted in this order first then alphabetically
|
||||||
|
*/
|
||||||
|
export function sortItemKeysByPriorityList(data: INodeExecutionData[], priorityList: string[]) {
|
||||||
|
return data.map((item) => {
|
||||||
|
const itemKeys = Object.keys(item.json);
|
||||||
|
|
||||||
|
const updatedKeysOrder = itemKeys.sort((a, b) => {
|
||||||
|
const indexA = priorityList.indexOf(a);
|
||||||
|
const indexB = priorityList.indexOf(b);
|
||||||
|
|
||||||
|
if (indexA !== -1 && indexB !== -1) {
|
||||||
|
return indexA - indexB;
|
||||||
|
} else if (indexA !== -1) {
|
||||||
|
return -1;
|
||||||
|
} else if (indexB !== -1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return a.localeCompare(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedItem: IDataObject = {};
|
||||||
|
for (const key of updatedKeysOrder) {
|
||||||
|
updatedItem[key] = item.json[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
item.json = updatedItem;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1404,7 +1404,7 @@ function addToIssuesIfMissing(
|
|||||||
if (
|
if (
|
||||||
(nodeProperties.type === 'string' && (value === '' || value === undefined)) ||
|
(nodeProperties.type === 'string' && (value === '' || value === undefined)) ||
|
||||||
(nodeProperties.type === 'multiOptions' && Array.isArray(value) && value.length === 0) ||
|
(nodeProperties.type === 'multiOptions' && Array.isArray(value) && value.length === 0) ||
|
||||||
(nodeProperties.type === 'dateTime' && value === undefined) ||
|
(nodeProperties.type === 'dateTime' && (value === '' || value === undefined)) ||
|
||||||
(nodeProperties.type === 'options' && (value === '' || value === undefined)) ||
|
(nodeProperties.type === 'options' && (value === '' || value === undefined)) ||
|
||||||
((nodeProperties.type === 'resourceLocator' || nodeProperties.type === 'workflowSelector') &&
|
((nodeProperties.type === 'resourceLocator' || nodeProperties.type === 'workflowSelector') &&
|
||||||
!isValidResourceLocatorParameterValue(value as INodeParameterResourceLocator))
|
!isValidResourceLocatorParameterValue(value as INodeParameterResourceLocator))
|
||||||
|
|||||||
@@ -4195,4 +4195,57 @@ describe('NodeHelpers', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getParameterIssues, required parameters validation', () => {
|
||||||
|
const testNode: INode = {
|
||||||
|
id: '12345',
|
||||||
|
name: 'Test Node',
|
||||||
|
typeVersion: 1,
|
||||||
|
type: 'n8n-nodes-base.testNode',
|
||||||
|
position: [1, 1],
|
||||||
|
parameters: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('Should validate required dateTime parameters if empty string', () => {
|
||||||
|
const nodeProperties: INodeProperties = {
|
||||||
|
displayName: 'Date Time',
|
||||||
|
name: 'testDateTime',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
};
|
||||||
|
const nodeValues: INodeParameters = {
|
||||||
|
testDateTime: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getParameterIssues(nodeProperties, nodeValues, '', testNode);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
parameters: {
|
||||||
|
testDateTime: ['Parameter "Date Time" is required.'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should validate required dateTime parameters if empty undefined', () => {
|
||||||
|
const nodeProperties: INodeProperties = {
|
||||||
|
displayName: 'Date Time',
|
||||||
|
name: 'testDateTime',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
};
|
||||||
|
const nodeValues: INodeParameters = {
|
||||||
|
testDateTime: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getParameterIssues(nodeProperties, nodeValues, '', testNode);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
parameters: {
|
||||||
|
testDateTime: ['Parameter "Date Time" is required.'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user