@guayaba/workflow-piece-google-calendar 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.babelrc +3 -0
  2. package/.eslintrc.json +18 -0
  3. package/README.md +5 -0
  4. package/assets/logo.png +0 -0
  5. package/package.json +23 -0
  6. package/src/i18n/ca.json +71 -0
  7. package/src/i18n/de.json +151 -0
  8. package/src/i18n/es.json +151 -0
  9. package/src/i18n/fr.json +151 -0
  10. package/src/i18n/hi.json +71 -0
  11. package/src/i18n/id.json +71 -0
  12. package/src/i18n/ja.json +151 -0
  13. package/src/i18n/nl.json +151 -0
  14. package/src/i18n/pt.json +151 -0
  15. package/src/i18n/ru.json +71 -0
  16. package/src/i18n/translation.json +151 -0
  17. package/src/i18n/vi.json +71 -0
  18. package/src/i18n/zh.json +151 -0
  19. package/src/index.ts +78 -0
  20. package/src/lib/actions/add-attendees.action.ts +52 -0
  21. package/src/lib/actions/add-calendar-to-calendarlist.ts +33 -0
  22. package/src/lib/actions/create-event.ts +163 -0
  23. package/src/lib/actions/create-quick-event.ts +66 -0
  24. package/src/lib/actions/delete-event.action.ts +32 -0
  25. package/src/lib/actions/find-busy-free-periods.ts +97 -0
  26. package/src/lib/actions/get-event-by-id.ts +123 -0
  27. package/src/lib/actions/get-events.ts +111 -0
  28. package/src/lib/actions/update-event.action.ts +126 -0
  29. package/src/lib/auth.ts +81 -0
  30. package/src/lib/common/helper.ts +228 -0
  31. package/src/lib/common/index.ts +95 -0
  32. package/src/lib/common/types.ts +246 -0
  33. package/src/lib/google-calendar.mdx +25 -0
  34. package/src/lib/triggers/calendar-event.ts +139 -0
  35. package/src/lib/triggers/event-cancelled.ts +200 -0
  36. package/src/lib/triggers/event-ends.ts +201 -0
  37. package/src/lib/triggers/event-start-time-before.ts +201 -0
  38. package/src/lib/triggers/new-calendar.ts +203 -0
  39. package/src/lib/triggers/new-event-matching-search.ts +225 -0
  40. package/src/lib/triggers/new-event.ts +184 -0
  41. package/tsconfig.json +16 -0
  42. package/tsconfig.lib.json +15 -0
@@ -0,0 +1,201 @@
1
+ import {
2
+ AppConnectionValueForAuthProperty,
3
+ createTrigger,
4
+ Property,
5
+ } from '@guayaba/workflows-framework';
6
+ import { TriggerStrategy } from '@guayaba/workflows-framework';
7
+ import { googleCalendarCommon, googleCalendarAuth, getAccessToken } from '../common';
8
+ import { GoogleCalendarEvent } from '../common/types';
9
+ import {
10
+ DedupeStrategy,
11
+ Polling,
12
+ pollingHelper,
13
+ } from '@guayaba/workflows-common';
14
+ import {
15
+ AuthenticationType,
16
+ httpClient,
17
+ HttpMethod,
18
+ HttpRequest,
19
+ } from '@guayaba/workflows-common';
20
+
21
+ interface GoogleCalendarEventList {
22
+ items: GoogleCalendarEvent[];
23
+ }
24
+
25
+ const polling: Polling<
26
+ AppConnectionValueForAuthProperty<typeof googleCalendarAuth>,
27
+ {
28
+ calendar_id: string | undefined;
29
+ specific_event: boolean | undefined;
30
+ event_id: string | undefined;
31
+ }
32
+ > = {
33
+ strategy: DedupeStrategy.TIMEBASED,
34
+ items: async ({ auth, propsValue, lastFetchEpochMS }) => {
35
+ if (lastFetchEpochMS === 0) {
36
+ return [];
37
+ }
38
+
39
+ const { calendar_id: calendarId, specific_event, event_id } = propsValue;
40
+
41
+ if (!calendarId) {
42
+ return [];
43
+ }
44
+
45
+ if (specific_event && !event_id) {
46
+ return [];
47
+ }
48
+
49
+ let events: GoogleCalendarEvent[] = [];
50
+
51
+ if (specific_event && event_id) {
52
+ const eventRequest: HttpRequest = {
53
+ method: HttpMethod.GET,
54
+ url: `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events/${event_id}`,
55
+ authentication: {
56
+ type: AuthenticationType.BEARER_TOKEN,
57
+ token: await getAccessToken(auth),
58
+ },
59
+ };
60
+
61
+ try {
62
+ const eventResponse = await httpClient.sendRequest<GoogleCalendarEvent>(
63
+ eventRequest
64
+ );
65
+ const event = eventResponse.body;
66
+
67
+ const endTimeString = event.end?.dateTime ?? event.end?.date;
68
+ if (endTimeString) {
69
+ const endTime = new Date(endTimeString).getTime();
70
+ if (endTime > lastFetchEpochMS) {
71
+ events = [event];
72
+ }
73
+ }
74
+ } catch (error) {
75
+ console.error('Error fetching specific event:', error);
76
+ return [];
77
+ }
78
+ } else {
79
+ const request: HttpRequest = {
80
+ method: HttpMethod.GET,
81
+ url: `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events`,
82
+ authentication: {
83
+ type: AuthenticationType.BEARER_TOKEN,
84
+ token: await getAccessToken(auth),
85
+ },
86
+ queryParams: {
87
+ singleEvents: 'true',
88
+ orderBy: 'startTime',
89
+ timeMin: new Date(lastFetchEpochMS).toISOString(),
90
+ },
91
+ };
92
+
93
+ const response = await httpClient.sendRequest<GoogleCalendarEventList>(
94
+ request
95
+ );
96
+ events = response.body.items;
97
+ }
98
+ const endedEvents: {
99
+ epochMilliSeconds: number;
100
+ data: GoogleCalendarEvent;
101
+ }[] = [];
102
+ const now = Date.now();
103
+
104
+ for (const event of events) {
105
+ const endTimeString = event.end?.dateTime ?? event.end?.date;
106
+ if (!endTimeString) continue;
107
+
108
+ const endTime = new Date(endTimeString).getTime();
109
+
110
+ if (endTime > lastFetchEpochMS && endTime <= now) {
111
+ endedEvents.push({
112
+ epochMilliSeconds: endTime,
113
+ data: event,
114
+ });
115
+ }
116
+ }
117
+
118
+ return endedEvents;
119
+ },
120
+ };
121
+
122
+ export const eventEnds = createTrigger({
123
+ auth: googleCalendarAuth,
124
+ name: 'event_ends',
125
+ displayName: 'Event Ends',
126
+ description: 'Fires when an event ends.',
127
+ props: {
128
+ calendar_id: googleCalendarCommon.calendarDropdown('writer'),
129
+ specific_event: Property.Checkbox({
130
+ displayName: 'Target Specific Event',
131
+ description:
132
+ 'Enable to monitor a specific event instead of all events in the calendar.',
133
+ required: false,
134
+ defaultValue: false,
135
+ }),
136
+ event_id: googleCalendarCommon.eventDropdown(false),
137
+ },
138
+ type: TriggerStrategy.POLLING,
139
+ sampleData: {
140
+ kind: 'calendar#event',
141
+ etag: '"3419997894982000"',
142
+ id: 'sample_event_id_67890',
143
+ status: 'confirmed',
144
+ htmlLink:
145
+ 'https://www.google.com/calendar/event?eid=c2FtcGxlX2V2ZW50X2lkXzY3ODkw',
146
+ created: '2025-08-14T09:00:00.000Z',
147
+ updated: '2025-08-14T09:00:00.000Z',
148
+ summary: 'Project Deadline',
149
+ creator: { email: 'manager@example.com' },
150
+ organizer: {
151
+ email: 'manager@example.com',
152
+ self: true,
153
+ },
154
+ start: {
155
+ dateTime: '2025-08-14T14:30:00+05:30',
156
+ timeZone: 'Asia/Kolkata',
157
+ },
158
+ end: {
159
+ dateTime: '2025-08-14T15:30:00+05:30',
160
+ timeZone: 'Asia/Kolkata',
161
+ },
162
+ iCalUID: 'sample_event_id_67890@google.com',
163
+ sequence: 0,
164
+ reminders: { useDefault: true },
165
+ eventType: 'default',
166
+ },
167
+
168
+ async onEnable(context) {
169
+ await pollingHelper.onEnable(polling, {
170
+ auth: context.auth,
171
+ store: context.store,
172
+ propsValue: context.propsValue,
173
+ });
174
+ },
175
+
176
+ async onDisable(context) {
177
+ await pollingHelper.onDisable(polling, {
178
+ auth: context.auth,
179
+ store: context.store,
180
+ propsValue: context.propsValue,
181
+ });
182
+ },
183
+
184
+ async run(context) {
185
+ return await pollingHelper.poll(polling, {
186
+ auth: context.auth,
187
+ store: context.store,
188
+ propsValue: context.propsValue,
189
+ files: context.files,
190
+ });
191
+ },
192
+
193
+ async test(context) {
194
+ return await pollingHelper.test(polling, {
195
+ auth: context.auth,
196
+ store: context.store,
197
+ propsValue: context.propsValue,
198
+ files: context.files,
199
+ });
200
+ },
201
+ });
@@ -0,0 +1,201 @@
1
+ import {
2
+ AppConnectionValueForAuthProperty,
3
+ createTrigger,
4
+ PiecePropValueSchema,
5
+ Property,
6
+ } from '@guayaba/workflows-framework';
7
+ import { TriggerStrategy } from '@guayaba/workflows-framework';
8
+ import { googleCalendarCommon, googleCalendarAuth, getAccessToken } from '../common';
9
+ import { GoogleCalendarEvent } from '../common/types';
10
+ import {
11
+ DedupeStrategy,
12
+ Polling,
13
+ pollingHelper,
14
+ } from '@guayaba/workflows-common';
15
+ import {
16
+ AuthenticationType,
17
+ httpClient,
18
+ HttpMethod,
19
+ HttpRequest,
20
+ } from '@guayaba/workflows-common';
21
+
22
+ interface GoogleCalendarEventList {
23
+ items: GoogleCalendarEvent[];
24
+ }
25
+
26
+ const polling: Polling<
27
+ AppConnectionValueForAuthProperty<typeof googleCalendarAuth>,
28
+ {
29
+ calendar_id: string | undefined;
30
+ specific_event: boolean | undefined;
31
+ event_id: string | undefined;
32
+ time_value: number | undefined;
33
+ time_unit: string | undefined;
34
+ }
35
+ > = {
36
+ strategy: DedupeStrategy.TIMEBASED,
37
+ items: async ({ auth, propsValue }) => {
38
+ const { calendar_id, specific_event, event_id, time_value, time_unit } =
39
+ propsValue;
40
+
41
+ if (!calendar_id || !time_value || !time_unit) {
42
+ return [];
43
+ }
44
+
45
+ if (specific_event && !event_id) {
46
+ return [];
47
+ }
48
+
49
+ let offset_ms = time_value * 60 * 1000;
50
+ if (time_unit === 'hours') {
51
+ offset_ms = time_value * 60 * 60 * 1000;
52
+ } else if (time_unit === 'days') {
53
+ offset_ms = time_value * 24 * 60 * 60 * 1000;
54
+ }
55
+
56
+ const now = Date.now();
57
+
58
+ const pollingIntervalMs = 5 * 60 * 1000;
59
+
60
+ const timeMin = new Date(now + offset_ms).toISOString();
61
+ const timeMax = new Date(now + offset_ms + pollingIntervalMs).toISOString();
62
+
63
+ let events: GoogleCalendarEvent[] = [];
64
+
65
+ if (specific_event && event_id) {
66
+ const eventRequest: HttpRequest = {
67
+ method: HttpMethod.GET,
68
+ url: `${googleCalendarCommon.baseUrl}/calendars/${calendar_id}/events/${event_id}`,
69
+ authentication: {
70
+ type: AuthenticationType.BEARER_TOKEN,
71
+ token: await getAccessToken(auth),
72
+ },
73
+ };
74
+
75
+ try {
76
+ const eventResponse = await httpClient.sendRequest<GoogleCalendarEvent>(
77
+ eventRequest
78
+ );
79
+ const event = eventResponse.body;
80
+
81
+ // Check if this specific event falls within our time window
82
+ const eventStartTime = new Date(
83
+ event.start?.dateTime ?? event.start?.date ?? 0
84
+ ).getTime();
85
+ const triggerTime = eventStartTime - offset_ms;
86
+
87
+ if (triggerTime >= now && triggerTime <= now + pollingIntervalMs) {
88
+ events = [event];
89
+ }
90
+ } catch (error) {
91
+ console.error('Error fetching specific event:', error);
92
+ return [];
93
+ }
94
+ } else {
95
+ const request: HttpRequest = {
96
+ method: HttpMethod.GET,
97
+ url: `${googleCalendarCommon.baseUrl}/calendars/${calendar_id}/events`,
98
+ authentication: {
99
+ type: AuthenticationType.BEARER_TOKEN,
100
+ token: await getAccessToken(auth),
101
+ },
102
+ queryParams: {
103
+ singleEvents: 'true',
104
+ orderBy: 'startTime',
105
+ timeMin: timeMin,
106
+ timeMax: timeMax,
107
+ },
108
+ };
109
+
110
+ const response = await httpClient.sendRequest<GoogleCalendarEventList>(
111
+ request
112
+ );
113
+ events = response.body.items;
114
+ }
115
+
116
+ return events.map((event) => {
117
+ const startTime = new Date(
118
+ event.start?.dateTime ?? event.start?.date ?? 0
119
+ ).getTime();
120
+
121
+ const triggerTime = startTime - offset_ms;
122
+ return {
123
+ epochMilliSeconds: triggerTime,
124
+ data: event,
125
+ };
126
+ });
127
+ },
128
+ };
129
+
130
+ export const eventStartTimeBefore = createTrigger({
131
+ auth: googleCalendarAuth,
132
+ name: 'event_starts_in',
133
+ displayName: 'Event Start (Time Before)',
134
+ description:
135
+ 'Fires at a specified amount of time before an event starts (e.g., a reminder).',
136
+ props: {
137
+ calendar_id: googleCalendarCommon.calendarDropdown('writer'),
138
+ specific_event: Property.Checkbox({
139
+ displayName: 'Target Specific Event',
140
+ description:
141
+ 'Enable to monitor a specific event instead of all events in the calendar.',
142
+ required: false,
143
+ defaultValue: false,
144
+ }),
145
+ event_id: googleCalendarCommon.eventDropdown(false),
146
+ time_value: Property.Number({
147
+ displayName: 'Time Before',
148
+ description: 'The amount of time before the event starts.',
149
+ required: true,
150
+ defaultValue: 15,
151
+ }),
152
+ time_unit: Property.StaticDropdown({
153
+ displayName: 'Time Unit',
154
+ required: true,
155
+ options: {
156
+ options: [
157
+ { label: 'Minutes', value: 'minutes' },
158
+ { label: 'Hours', value: 'hours' },
159
+ { label: 'Days', value: 'days' },
160
+ ],
161
+ },
162
+ defaultValue: 'minutes',
163
+ }),
164
+ },
165
+ type: TriggerStrategy.POLLING,
166
+ sampleData: {},
167
+
168
+ async onEnable(context) {
169
+ await pollingHelper.onEnable(polling, {
170
+ auth: context.auth,
171
+ store: context.store,
172
+ propsValue: context.propsValue,
173
+ });
174
+ },
175
+
176
+ async onDisable(context) {
177
+ await pollingHelper.onDisable(polling, {
178
+ auth: context.auth,
179
+ store: context.store,
180
+ propsValue: context.propsValue,
181
+ });
182
+ },
183
+
184
+ async run(context) {
185
+ return await pollingHelper.poll(polling, {
186
+ auth: context.auth,
187
+ store: context.store,
188
+ propsValue: context.propsValue,
189
+ files: context.files,
190
+ });
191
+ },
192
+
193
+ async test(context) {
194
+ return await pollingHelper.test(polling, {
195
+ auth: context.auth,
196
+ store: context.store,
197
+ propsValue: context.propsValue,
198
+ files: context.files,
199
+ });
200
+ },
201
+ });
@@ -0,0 +1,203 @@
1
+ import {
2
+ createTrigger,
3
+ AppConnectionValueForAuthProperty,
4
+ Property,
5
+ } from '@guayaba/workflows-framework';
6
+ import { TriggerStrategy } from '@guayaba/workflows-framework';
7
+ import { googleCalendarAuth } from '../common';
8
+ import {
9
+ DedupeStrategy,
10
+ Polling,
11
+ pollingHelper,
12
+ } from '@guayaba/workflows-common';
13
+ import { getCalendars } from '../common/helper';
14
+ import { CalendarObject } from '../common/types';
15
+
16
+ const polling: Polling<
17
+ AppConnectionValueForAuthProperty<typeof googleCalendarAuth>,
18
+ {
19
+ access_role_filter: string[] | undefined;
20
+ calendar_name_filter: string | undefined;
21
+ exclude_shared: boolean | undefined;
22
+ }
23
+ > = {
24
+ strategy: DedupeStrategy.LAST_ITEM,
25
+ items: async ({ auth, store, propsValue }) => {
26
+ const { access_role_filter, calendar_name_filter, exclude_shared } =
27
+ propsValue;
28
+
29
+ const currentCalendars = await getCalendars(auth);
30
+
31
+ let filteredCalendars = currentCalendars;
32
+
33
+ if (access_role_filter && access_role_filter.length > 0) {
34
+ filteredCalendars = filteredCalendars.filter((cal) =>
35
+ access_role_filter.includes(cal.accessRole)
36
+ );
37
+ }
38
+
39
+ if (calendar_name_filter && calendar_name_filter.trim()) {
40
+ const searchTerm = calendar_name_filter.toLowerCase().trim();
41
+ filteredCalendars = filteredCalendars.filter(
42
+ (cal) =>
43
+ cal.summary?.toLowerCase().includes(searchTerm) ||
44
+ cal.description?.toLowerCase().includes(searchTerm)
45
+ );
46
+ }
47
+
48
+ if (exclude_shared) {
49
+ filteredCalendars = filteredCalendars.filter(
50
+ (cal) => cal.accessRole === 'owner' || cal.primary
51
+ );
52
+ }
53
+
54
+ const currentCalendarIds = filteredCalendars.map((cal) => cal.id);
55
+
56
+ const oldCalendarIds = (await store.get<string[]>('calendars')) || [];
57
+ const oldCalendarIdsSet = new Set(oldCalendarIds);
58
+
59
+ const newCalendars = filteredCalendars.filter(
60
+ (cal) => !oldCalendarIdsSet.has(cal.id)
61
+ );
62
+
63
+ await store.put('calendars', currentCalendarIds);
64
+
65
+ return newCalendars.map((cal) => ({
66
+ id: cal.id,
67
+ data: {
68
+ ...cal,
69
+ isOwned: cal.accessRole === 'owner' || cal.primary,
70
+ isShared: cal.accessRole !== 'owner' && !cal.primary,
71
+ calendarType: cal.primary
72
+ ? 'primary'
73
+ : cal.accessRole === 'owner'
74
+ ? 'owned'
75
+ : 'shared',
76
+ },
77
+ }));
78
+ },
79
+ };
80
+
81
+ export const newCalendar = createTrigger({
82
+ auth: googleCalendarAuth,
83
+ name: 'new_calendar',
84
+ displayName: 'New Calendar',
85
+ description: 'Fires when a new calendar is created or becomes accessible.',
86
+ props: {
87
+ access_role_filter: Property.StaticMultiSelectDropdown({
88
+ displayName: 'Access Role Filter',
89
+ description:
90
+ 'Only trigger for calendars with specific access roles (optional)',
91
+ required: false,
92
+ options: {
93
+ options: [
94
+ { label: 'Owner', value: 'owner' },
95
+ { label: 'Writer', value: 'writer' },
96
+ { label: 'Reader', value: 'reader' },
97
+ { label: 'Free/Busy Reader', value: 'freeBusyReader' },
98
+ ],
99
+ },
100
+ }),
101
+ calendar_name_filter: Property.ShortText({
102
+ displayName: 'Calendar Name Filter',
103
+ description:
104
+ 'Only trigger for calendars containing this text in name or description (optional)',
105
+ required: false,
106
+ }),
107
+ exclude_shared: Property.Checkbox({
108
+ displayName: 'Exclude Shared Calendars',
109
+ description: 'Only trigger for calendars you own, not shared calendars',
110
+ required: false,
111
+ defaultValue: false,
112
+ }),
113
+ },
114
+ type: TriggerStrategy.POLLING,
115
+ sampleData: {
116
+ id: 'sample_calendar_id@group.calendar.google.com',
117
+ summary: 'New Project Team Calendar',
118
+ description: 'A shared calendar for the new project team.',
119
+ timeZone: 'Asia/Kolkata',
120
+ backgroundColor: '#9fe1e7',
121
+ foregroundColor: '#000000',
122
+ accessRole: 'owner',
123
+ isOwned: true,
124
+ isShared: false,
125
+ calendarType: 'owned',
126
+ primary: false,
127
+ },
128
+
129
+ async onEnable(context) {
130
+ const calendars = await getCalendars(context.auth);
131
+
132
+ const { access_role_filter, calendar_name_filter, exclude_shared } =
133
+ context.propsValue;
134
+ let filteredCalendars = calendars;
135
+
136
+ if (access_role_filter && access_role_filter.length > 0) {
137
+ filteredCalendars = filteredCalendars.filter((cal) =>
138
+ access_role_filter.includes(cal.accessRole)
139
+ );
140
+ }
141
+
142
+ if (calendar_name_filter && calendar_name_filter.trim()) {
143
+ const searchTerm = calendar_name_filter.toLowerCase().trim();
144
+ filteredCalendars = filteredCalendars.filter(
145
+ (cal) =>
146
+ cal.summary?.toLowerCase().includes(searchTerm) ||
147
+ cal.description?.toLowerCase().includes(searchTerm)
148
+ );
149
+ }
150
+
151
+ if (exclude_shared) {
152
+ filteredCalendars = filteredCalendars.filter(
153
+ (cal) => cal.accessRole === 'owner' || cal.primary
154
+ );
155
+ }
156
+
157
+ await context.store.put(
158
+ 'calendars',
159
+ filteredCalendars.map((cal) => cal.id)
160
+ );
161
+
162
+ await pollingHelper.onEnable(polling, {
163
+ auth: context.auth,
164
+ store: context.store,
165
+ propsValue: context.propsValue,
166
+ });
167
+ },
168
+
169
+ async onDisable(context) {
170
+ await pollingHelper.onDisable(polling, {
171
+ auth: context.auth,
172
+ store: context.store,
173
+ propsValue: context.propsValue,
174
+ });
175
+ },
176
+
177
+ async run(context) {
178
+ return await pollingHelper.poll(polling, {
179
+ auth: context.auth,
180
+ store: context.store,
181
+ propsValue: context.propsValue,
182
+ files: context.files,
183
+ });
184
+ },
185
+
186
+ async test(context) {
187
+ const calendars = await getCalendars(context.auth);
188
+ const recentCalendars = calendars.slice(-1);
189
+ return recentCalendars.map((cal: CalendarObject) => ({
190
+ id: cal.id,
191
+ data: {
192
+ ...cal,
193
+ isOwned: cal.accessRole === 'owner' || cal.primary,
194
+ isShared: cal.accessRole !== 'owner' && !cal.primary,
195
+ calendarType: cal.primary
196
+ ? 'primary'
197
+ : cal.accessRole === 'owner'
198
+ ? 'owned'
199
+ : 'shared',
200
+ },
201
+ }));
202
+ },
203
+ });