@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,126 @@
1
+ import { Property, createAction } from '@guayaba/workflows-framework';
2
+ import { google, calendar_v3 } from 'googleapis';
3
+ import { googleCalendarCommon, googleCalendarAuth, createGoogleClient } from '../common';
4
+ import dayjs from 'dayjs';
5
+
6
+ export const updateEventAction = createAction({
7
+ displayName: 'Update Event',
8
+ auth: googleCalendarAuth,
9
+ name: 'update_event',
10
+ description: 'Updates an event in Google Calendar.',
11
+ props: {
12
+ calendar_id: googleCalendarCommon.calendarDropdown('writer'),
13
+ eventId: Property.ShortText({
14
+ displayName: 'Event ID',
15
+ required: true,
16
+ }),
17
+ title: Property.ShortText({
18
+ displayName: 'Title of the event',
19
+ required: false,
20
+ }),
21
+ start_date_time: Property.DateTime({
22
+ displayName: 'Start date time of the event',
23
+ required: false,
24
+ }),
25
+ end_date_time: Property.DateTime({
26
+ displayName: 'End date time of the event',
27
+ required: false,
28
+ }),
29
+ location: Property.ShortText({
30
+ displayName: 'Location',
31
+ required: false,
32
+ }),
33
+ description: Property.LongText({
34
+ displayName: 'Description',
35
+ description: 'Description of the event. You can use HTML tags here.',
36
+ required: false,
37
+ }),
38
+ colorId: googleCalendarCommon.colorId,
39
+ attendees: Property.Array({
40
+ displayName: 'Attendees',
41
+ description: 'Emails of the attendees (guests)',
42
+ required: false,
43
+ }),
44
+ guests_can_modify: Property.Checkbox({
45
+ displayName: 'Guests can modify',
46
+ defaultValue: false,
47
+ required: false,
48
+ }),
49
+ guests_can_invite_others: Property.Checkbox({
50
+ displayName: 'Guests can invite others',
51
+ defaultValue: false,
52
+ required: false,
53
+ }),
54
+ guests_can_see_other_guests: Property.Checkbox({
55
+ displayName: 'Guests can see other guests',
56
+ defaultValue: false,
57
+ required: false,
58
+ }),
59
+ },
60
+ async run(context) {
61
+ const {
62
+ calendar_id,
63
+ eventId,
64
+ title,
65
+ start_date_time,
66
+ end_date_time,
67
+ location,
68
+ description,
69
+ colorId,
70
+ guests_can_invite_others,
71
+ guests_can_modify,
72
+ guests_can_see_other_guests,
73
+ } = context.propsValue;
74
+
75
+ const attendees = context.propsValue.attendees as string[];
76
+
77
+ const authClient = await createGoogleClient(context.auth);
78
+ const calendar = google.calendar({ version: 'v3', auth: authClient });
79
+
80
+ // Note that each patch request consumes three quota units;
81
+ // prefer using a get followed by an update
82
+ const currentEvent = await calendar.events.get({
83
+ calendarId: calendar_id,
84
+ eventId: eventId,
85
+ });
86
+
87
+ let attendeeFormattedList: calendar_v3.Schema$EventAttendee[] = [];
88
+ if (Array.isArray(attendees) && attendees.length > 0) {
89
+ attendeeFormattedList = attendees.map((email) => ({ email }));
90
+ } else if (
91
+ currentEvent.data.attendees &&
92
+ Array.isArray(currentEvent.data.attendees)
93
+ ) {
94
+ attendeeFormattedList = currentEvent.data.attendees;
95
+ }
96
+
97
+ const response = await calendar.events.update({
98
+ calendarId: calendar_id,
99
+ eventId: eventId,
100
+ requestBody: {
101
+ summary: title ?? currentEvent.data.summary,
102
+ attendees: attendeeFormattedList,
103
+ description: description ?? currentEvent.data.description,
104
+ colorId: colorId,
105
+ location: location ?? currentEvent.data.location,
106
+ start: start_date_time
107
+ ? {
108
+ dateTime: dayjs(start_date_time).format(
109
+ 'YYYY-MM-DDTHH:mm:ss.sssZ'
110
+ ),
111
+ }
112
+ : currentEvent.data.start,
113
+ end: end_date_time
114
+ ? {
115
+ dateTime: dayjs(end_date_time).format('YYYY-MM-DDTHH:mm:ss.sssZ'),
116
+ }
117
+ : currentEvent.data.end,
118
+ guestsCanInviteOthers: guests_can_invite_others,
119
+ guestsCanModify: guests_can_modify,
120
+ guestsCanSeeOtherGuests: guests_can_see_other_guests,
121
+ },
122
+ });
123
+
124
+ return response.data;
125
+ },
126
+ });
@@ -0,0 +1,81 @@
1
+ import { AppConnectionValueForAuthProperty, PieceAuth, Property } from '@guayaba/workflows-framework';
2
+ import { AppConnectionType } from '@guayaba/workflows-shared';
3
+ import { google } from 'googleapis';
4
+ import { OAuth2Client } from 'googleapis-common';
5
+
6
+ export const googleCalendarScopes = [
7
+ 'https://www.googleapis.com/auth/calendar.events',
8
+ 'https://www.googleapis.com/auth/calendar.readonly',
9
+ // TODO: Add the scope after Google App Verification
10
+ // 'https://www.googleapis.com/auth/calendar.calendarlist'
11
+ ];
12
+
13
+ export const googleCalendarAuth = [PieceAuth.OAuth2({
14
+ description: '',
15
+ authUrl: 'https://accounts.google.com/o/oauth2/auth',
16
+ tokenUrl: 'https://oauth2.googleapis.com/token',
17
+ required: true,
18
+ pkce: true,
19
+ scope: googleCalendarScopes,
20
+ }), PieceAuth.CustomAuth({
21
+ displayName: 'Service Account (Advanced)',
22
+ description: 'Authenticate via service account from https://console.cloud.google.com/ > IAM & Admin > Service Accounts > Create Service Account > Keys > Add key. <br> <br> You can optionally use domain-wide delegation (https://support.google.com/a/answer/162106?hl=en#zippy=%2Cset-up-domain-wide-delegation-for-a-client) to access calendars without adding the service account to each one. <br> <br> **Note:** Without a user email, the service account only has access to calendars you explicitly share with it.',
23
+ required: true,
24
+ props: {
25
+ serviceAccount: Property.ShortText({
26
+ displayName: 'Service Account JSON Key',
27
+ required: true,
28
+ }),
29
+ userEmail: Property.ShortText({
30
+ displayName: 'User Email',
31
+ required: false,
32
+ description: 'Email address of the user to impersonate for domain-wide delegation.',
33
+ }),
34
+ },
35
+ validate: async ({ auth }) => {
36
+ try {
37
+ await getAccessToken({
38
+ type: AppConnectionType.CUSTOM_AUTH,
39
+ props: { ...auth },
40
+ });
41
+ } catch (e) {
42
+ return {
43
+ valid: false,
44
+ error: (e as Error).message,
45
+ };
46
+ }
47
+ return {
48
+ valid: true,
49
+ };
50
+ },
51
+ })];
52
+
53
+ export type GoogleCalendarAuthValue = AppConnectionValueForAuthProperty<typeof googleCalendarAuth>;
54
+
55
+ export async function createGoogleClient(auth: GoogleCalendarAuthValue): Promise<OAuth2Client> {
56
+ if (auth.type === AppConnectionType.CUSTOM_AUTH) {
57
+ const serviceAccount = JSON.parse(auth.props.serviceAccount);
58
+ return new google.auth.JWT({
59
+ email: serviceAccount.client_email,
60
+ key: serviceAccount.private_key,
61
+ scopes: googleCalendarScopes,
62
+ subject: auth.props.userEmail,
63
+ });
64
+ }
65
+ const authClient = new OAuth2Client();
66
+ authClient.setCredentials(auth);
67
+ return authClient;
68
+ }
69
+
70
+ export const getAccessToken = async (auth: GoogleCalendarAuthValue): Promise<string> => {
71
+ if (auth.type === AppConnectionType.CUSTOM_AUTH) {
72
+ const googleClient = await createGoogleClient(auth);
73
+ const response = await googleClient.getAccessToken();
74
+ if (response.token) {
75
+ return response.token;
76
+ } else {
77
+ throw new Error('Could not retrieve access token from service account json');
78
+ }
79
+ }
80
+ return auth.access_token;
81
+ };
@@ -0,0 +1,228 @@
1
+ import {
2
+ AuthenticationType,
3
+ httpClient,
4
+ HttpMethod,
5
+ HttpRequest,
6
+ } from '@guayaba/workflows-common';
7
+ import { randomUUID } from 'crypto';
8
+ import { googleCalendarCommon, GoogleCalendarAuthValue, getAccessToken } from '.';
9
+ import {
10
+ GoogleWatchResponse,
11
+ GoogleWatchType,
12
+ CalendarObject,
13
+ CalendarList,
14
+ GoogleCalendarEvent,
15
+ GoogleCalendarEventList,
16
+ GetColorsResponse,
17
+ } from './types';
18
+
19
+ export async function stopWatchEvent(
20
+ body: GoogleWatchResponse,
21
+ authProp: GoogleCalendarAuthValue
22
+ ) {
23
+ const request: HttpRequest = {
24
+ method: HttpMethod.POST,
25
+ url: `${googleCalendarCommon.baseUrl}/channels/stop`,
26
+ body: {
27
+ id: body?.id,
28
+ resourceId: body?.resourceId,
29
+ },
30
+ authentication: {
31
+ type: AuthenticationType.BEARER_TOKEN,
32
+ token: await getAccessToken(authProp),
33
+ },
34
+ };
35
+ await httpClient.sendRequest<any>(request);
36
+ }
37
+
38
+ export async function watchEvent(
39
+ calendarId: string,
40
+ webhookUrl: string,
41
+ authProp: GoogleCalendarAuthValue
42
+ ): Promise<GoogleWatchResponse> {
43
+ const request: HttpRequest = {
44
+ method: HttpMethod.POST,
45
+ url: `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events/watch`,
46
+ body: {
47
+ id: randomUUID(),
48
+ type: GoogleWatchType.WEBHOOK,
49
+ address: webhookUrl,
50
+ },
51
+ authentication: {
52
+ type: AuthenticationType.BEARER_TOKEN,
53
+ token: await getAccessToken(authProp),
54
+ },
55
+ };
56
+ const { body: webhook } = await httpClient.sendRequest<GoogleWatchResponse>(
57
+ request
58
+ );
59
+ return webhook;
60
+ }
61
+
62
+ export async function getCalendars(
63
+ authProp: GoogleCalendarAuthValue,
64
+ minAccessRole?: 'writer'
65
+ ): Promise<CalendarObject[]> {
66
+ // docs: https://developers.google.com/calendar/api/v3/reference/calendarList/list
67
+ const queryParams: Record<string, string> = {
68
+ showDeleted: 'false',
69
+ };
70
+ if (minAccessRole) {
71
+ queryParams['minAccessRole'] = minAccessRole;
72
+ }
73
+ const request: HttpRequest = {
74
+ method: HttpMethod.GET,
75
+ url: `${googleCalendarCommon.baseUrl}/users/me/calendarList`,
76
+ queryParams: queryParams,
77
+ authentication: {
78
+ type: AuthenticationType.BEARER_TOKEN,
79
+ token: await getAccessToken(authProp),
80
+ },
81
+ };
82
+ const response = await httpClient.sendRequest<CalendarList>(request);
83
+ return response.body.items;
84
+ }
85
+
86
+ export async function getColors(
87
+ authProp: GoogleCalendarAuthValue
88
+ ): Promise<GetColorsResponse> {
89
+ const request: HttpRequest = {
90
+ method: HttpMethod.GET,
91
+ url: `${googleCalendarCommon.baseUrl}/colors`,
92
+ authentication: {
93
+ type: AuthenticationType.BEARER_TOKEN,
94
+ token: await getAccessToken(authProp),
95
+ },
96
+ };
97
+ const response = await httpClient.sendRequest<GetColorsResponse>(request);
98
+ return response.body;
99
+ }
100
+
101
+ export async function getEvents(
102
+ calendarId: string,
103
+ expandRecurringEvent: boolean,
104
+ authProp: GoogleCalendarAuthValue,
105
+ minUpdated?: Date
106
+ ): Promise<GoogleCalendarEvent[]> {
107
+ // docs: https://developers.google.com/calendar/api/v3/reference/events/list
108
+ const now = new Date();
109
+ const yesterday = new Date();
110
+ yesterday.setDate(now.getDate() - 1);
111
+
112
+ const qParams: Record<string, string> = {
113
+ updatedMin: minUpdated?.toISOString() ?? yesterday.toISOString(),
114
+ maxResults: '2500', // Modified
115
+ orderBy: 'updated',
116
+ singleEvents: expandRecurringEvent ? 'true' : 'false',
117
+ showDeleted: 'true',
118
+ };
119
+
120
+ const accessToken = await getAccessToken(authProp);
121
+ const request: HttpRequest = {
122
+ method: HttpMethod.GET,
123
+ url: `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events`,
124
+ queryParams: qParams,
125
+ authentication: {
126
+ type: AuthenticationType.BEARER_TOKEN,
127
+ token: accessToken,
128
+ },
129
+ };
130
+
131
+ let eventList: GoogleCalendarEvent[] = [];
132
+ let pageToken = '';
133
+ do {
134
+ qParams['pageToken'] = pageToken;
135
+ const { body: res } = await httpClient.sendRequest<GoogleCalendarEventList>(
136
+ request
137
+ );
138
+ if (res.items.length > 0) {
139
+ eventList = [...eventList, ...res.items];
140
+ }
141
+ pageToken = res.nextPageToken;
142
+ } while (pageToken);
143
+
144
+ return eventList;
145
+ }
146
+
147
+ export async function getLatestEvent(
148
+ calendarId: string,
149
+ authProp: GoogleCalendarAuthValue
150
+ ): Promise<GoogleCalendarEvent> {
151
+ const eventList = await getEvents(calendarId, false, authProp);
152
+ const lastUpdatedEvent = eventList.pop()!; // You can retrieve the last updated event.
153
+ return lastUpdatedEvent;
154
+ }
155
+
156
+ export async function getEventsForDropdown(
157
+ authProp: GoogleCalendarAuthValue,
158
+ calendarId?: string,
159
+ maxResults = 50
160
+ ): Promise<{ label: string; value: string }[]> {
161
+ if (!calendarId) {
162
+ return [];
163
+ }
164
+
165
+ try {
166
+ const now = new Date();
167
+ const futureDate = new Date();
168
+ futureDate.setDate(now.getDate() + 30);
169
+
170
+ const queryParams: Record<string, string> = {
171
+ singleEvents: 'true',
172
+ orderBy: 'startTime',
173
+ timeMin: now.toISOString(),
174
+ timeMax: futureDate.toISOString(),
175
+ maxResults: maxResults.toString(),
176
+ showDeleted: 'false',
177
+ };
178
+
179
+ const request: HttpRequest = {
180
+ method: HttpMethod.GET,
181
+ url: `${googleCalendarCommon.baseUrl}/calendars/${encodeURIComponent(
182
+ calendarId
183
+ )}/events`,
184
+ queryParams: queryParams,
185
+ authentication: {
186
+ type: AuthenticationType.BEARER_TOKEN,
187
+ token: await getAccessToken(authProp),
188
+ },
189
+ };
190
+
191
+ const response = await httpClient.sendRequest<GoogleCalendarEventList>(
192
+ request
193
+ );
194
+
195
+ if (!response.body.items || response.body.items.length === 0) {
196
+ return [];
197
+ }
198
+
199
+ return response.body.items
200
+ .map((event) => {
201
+ const startTime = event.start?.dateTime || event.start?.date || '';
202
+ const startDate = startTime
203
+ ? new Date(startTime).toLocaleDateString()
204
+ : '';
205
+ const startTimeFormatted = startTime
206
+ ? new Date(startTime).toLocaleTimeString()
207
+ : '';
208
+
209
+ let label = event.summary || 'Untitled Event';
210
+ if (startDate) {
211
+ label += ` (${startDate}`;
212
+ if (event.start?.dateTime) {
213
+ label += ` at ${startTimeFormatted}`;
214
+ }
215
+ label += ')';
216
+ }
217
+
218
+ return {
219
+ label: label,
220
+ value: event.id || '',
221
+ };
222
+ })
223
+ .filter((item) => item.value !== '');
224
+ } catch (error) {
225
+ console.error('Error fetching events for dropdown:', error);
226
+ return [];
227
+ }
228
+ }
@@ -0,0 +1,95 @@
1
+ import { Property } from '@guayaba/workflows-framework';
2
+ import { getCalendars, getColors, getEventsForDropdown } from './helper';
3
+ import { googleCalendarAuth, GoogleCalendarAuthValue, getAccessToken } from '../auth';
4
+
5
+ export { googleCalendarAuth, GoogleCalendarAuthValue, getAccessToken, createGoogleClient, googleCalendarScopes } from '../auth';
6
+
7
+ export const googleCalendarCommon = {
8
+ baseUrl: 'https://www.googleapis.com/calendar/v3',
9
+ calendarDropdown: (minAccessRole?: 'writer') => {
10
+ return Property.Dropdown<string,true,typeof googleCalendarAuth>({
11
+ auth: googleCalendarAuth,
12
+ displayName: 'Calendar',
13
+ refreshers: [],
14
+ required: true,
15
+ options: async ({ auth }) => {
16
+ if (!auth) {
17
+ return {
18
+ disabled: true,
19
+ placeholder: 'Please connect your account first',
20
+ options: [],
21
+ };
22
+ }
23
+ const authValue = auth as GoogleCalendarAuthValue;
24
+ const calendars = await getCalendars(authValue, minAccessRole);
25
+ return {
26
+ disabled: false,
27
+ options: calendars.map((calendar) => {
28
+ return {
29
+ label: calendar.summary,
30
+ value: calendar.id,
31
+ };
32
+ }),
33
+ };
34
+ },
35
+ });
36
+ },
37
+ eventDropdown: (required = false) => {
38
+ return Property.Dropdown<string,boolean,typeof googleCalendarAuth>({
39
+ displayName: 'Event',
40
+ refreshers: ['calendar_id'],
41
+ required: required,
42
+ auth: googleCalendarAuth,
43
+ options: async ({ auth, calendar_id }) => {
44
+ if (!auth) {
45
+ return {
46
+ disabled: true,
47
+ placeholder: 'Please connect your account first',
48
+ options: [],
49
+ };
50
+ }
51
+ if (!calendar_id) {
52
+ return {
53
+ disabled: true,
54
+ placeholder: 'Please select a calendar first',
55
+ options: [],
56
+ };
57
+ }
58
+ const authValue = auth as GoogleCalendarAuthValue;
59
+ const events = await getEventsForDropdown(
60
+ authValue,
61
+ calendar_id as string
62
+ );
63
+ return {
64
+ disabled: false,
65
+ options: events,
66
+ };
67
+ },
68
+ });
69
+ },
70
+ colorId: Property.Dropdown({
71
+ auth: googleCalendarAuth,
72
+ displayName: 'Color',
73
+ refreshers: [],
74
+ required: false,
75
+ options: async ({ auth }) => {
76
+ if (!auth) {
77
+ return {
78
+ disabled: true,
79
+ placeholder: 'Please connect your account first',
80
+ options: [],
81
+ };
82
+ }
83
+ const response = await getColors(auth as GoogleCalendarAuthValue);
84
+ return {
85
+ disabled: false,
86
+ options: Object.entries(response.event).map(([key, value]) => {
87
+ return {
88
+ label: value.background,
89
+ value: key,
90
+ };
91
+ }),
92
+ };
93
+ },
94
+ }),
95
+ };