@mcpher/gas-fakes 1.2.27 → 1.2.28

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.
package/package.json CHANGED
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "name": "@mcpher/gas-fakes",
36
36
  "author": "bruce mcpherson",
37
- "version": "1.2.27",
37
+ "version": "1.2.28",
38
38
  "license": "MIT",
39
39
  "main": "main.js",
40
40
  "description": "A proof of concept implementation of Apps Script Environment on Node",
package/src/cli/setup.js CHANGED
@@ -171,6 +171,11 @@ export async function initializeConfiguration(options = {}) {
171
171
  value: "https://www.googleapis.com/auth/gmail.modify",
172
172
  },
173
173
  */
174
+ {
175
+ sensitivity: "sensitive",
176
+ title: "Calendar (full access)",
177
+ value: "https://www.googleapis.com/auth/calendar",
178
+ },
174
179
  {
175
180
  // actually labels are not sensitive
176
181
  title: "Gmail labels",
@@ -302,8 +307,8 @@ export async function initializeConfiguration(options = {}) {
302
307
  existingConfig.LOG_DESTINATION
303
308
  ) > -1
304
309
  ? ["CONSOLE", "CLOUD", "BOTH", "NONE"].indexOf(
305
- existingConfig.LOG_DESTINATION
306
- )
310
+ existingConfig.LOG_DESTINATION
311
+ )
307
312
  : 0,
308
313
  },
309
314
  {
@@ -316,7 +321,7 @@ export async function initializeConfiguration(options = {}) {
316
321
  ],
317
322
  initial:
318
323
  ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE?.toUpperCase()) >
319
- -1
324
+ -1
320
325
  ? ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE.toUpperCase())
321
326
  : 0,
322
327
  },
@@ -486,13 +491,14 @@ export async function authenticateUser() {
486
491
 
487
492
  console.log("Revoking previous credentials...");
488
493
  try {
489
- execSync("gcloud auth revoke --quiet", { stdio: "ignore" });
494
+ execSync("gcloud auth revoke --quiet", { stdio: "ignore", shell: true });
490
495
  } catch (e) {
491
496
  /* ignore */
492
497
  }
493
498
  try {
494
499
  execSync("gcloud auth application-default revoke --quiet", {
495
500
  stdio: "ignore",
501
+ shell: true,
496
502
  });
497
503
  } catch (e) {
498
504
  /* ignore */
@@ -502,6 +508,7 @@ export async function authenticateUser() {
502
508
  try {
503
509
  execSync(`gcloud config configurations describe "${activeConfig}"`, {
504
510
  stdio: "ignore",
511
+ shell: true,
505
512
  });
506
513
  console.log(`Configuration '${activeConfig}' already exists.`);
507
514
  } catch (error) {
@@ -519,6 +526,26 @@ export async function authenticateUser() {
519
526
  console.log("Initiating user login...");
520
527
  runCommandSync(`gcloud auth login ${driveAccessFlag}`);
521
528
 
529
+ // --- Verify that the user is actually logged in ---
530
+ try {
531
+ const currentAccount = execSync("gcloud config get-value account", {
532
+ encoding: "utf8",
533
+ shell: true,
534
+ }).trim();
535
+
536
+ if (!currentAccount || currentAccount === "(unset)") {
537
+ console.error("\n[Error] Login appeared to fail or no account selected.");
538
+ console.error(
539
+ "Please try running 'gcloud auth login' manually to diagnose issues."
540
+ );
541
+ process.exit(1);
542
+ }
543
+ console.log(`Successfully logged in as: ${currentAccount}`);
544
+ } catch (error) {
545
+ console.error("\n[Error] Failed to verify logged-in account.");
546
+ process.exit(1);
547
+ }
548
+
522
549
  console.log("Initiating Application Default Credentials (ADC) login...");
523
550
  runCommandSync(
524
551
  `gcloud auth application-default login --scopes="${scopes}" ${clientFlag}`
@@ -543,7 +570,7 @@ export async function authenticateUser() {
543
570
  );
544
571
  }
545
572
 
546
- const currentProject = execSync("gcloud config get project")
573
+ const currentProject = execSync("gcloud config get project", { shell: true })
547
574
  .toString()
548
575
  .trim();
549
576
  console.log(
@@ -551,11 +578,12 @@ export async function authenticateUser() {
551
578
  );
552
579
 
553
580
  console.log("\nFetching token information...");
554
- const userToken = execSync("gcloud auth print-access-token")
581
+ const userToken = execSync("gcloud auth print-access-token", { shell: true })
555
582
  .toString()
556
583
  .trim();
557
584
  const appDefaultToken = execSync(
558
- "gcloud auth application-default print-access-token"
585
+ "gcloud auth application-default print-access-token",
586
+ { shell: true }
559
587
  )
560
588
  .toString()
561
589
  .trim();
@@ -586,6 +614,7 @@ export function enableGoogleAPIs(options) {
586
614
  docs: "docs.googleapis.com",
587
615
  gmail: "gmail.googleapis.com",
588
616
  logging: "logging.googleapis.com",
617
+ calendar: "calendar"
589
618
  };
590
619
 
591
620
  const servicesToEnable = new Set();
package/src/cli/utils.js CHANGED
@@ -4,7 +4,7 @@ const require = createRequire(import.meta.url);
4
4
  const pjson = require("../../package.json");
5
5
 
6
6
  export const VERSION = pjson.version;
7
- export const CLI_VERSION = "0.0.18"; // Kept from original logic
7
+ export const CLI_VERSION = "0.0.19";
8
8
  export const MCP_VERSION = "0.0.7";
9
9
 
10
10
  /**
@@ -25,7 +25,8 @@ export function normalizeScriptNewlines(text) {
25
25
  */
26
26
  export function spawnCommand(command, args) {
27
27
  return new Promise((resolve, reject) => {
28
- const child = spawn(command, args);
28
+ // shell: true is important for Windows to find .cmd/.bat files correctly
29
+ const child = spawn(command, args, { shell: true });
29
30
  let stdout = "";
30
31
  let stderr = "";
31
32
 
@@ -74,7 +75,9 @@ export async function checkForGcloudCli() {
74
75
  */
75
76
  export function runCommandSync(command) {
76
77
  try {
77
- execSync(command, { stdio: "inherit" });
78
+ // shell: true is explicitly added to ensure compatibility on Windows,
79
+ // especially for handling interactive commands or batch files like gcloud.cmd
80
+ execSync(command, { stdio: "inherit", shell: true });
78
81
  } catch (error) {
79
82
  console.error(`\nError executing command: ${command}`);
80
83
  process.exit(1);
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ import './services/urlfetchapp/app.js'
7
7
  import './services/utilities/app.js'
8
8
  import './services/spreadsheetapp/app.js'
9
9
  import './services/gmailapp/app.js'
10
+ import './services/calendarapp/app.js'
10
11
  import './services/session/app.js'
11
12
  import './services/advdrive/app.js'
12
13
  import './services/advsheets/app.js'
@@ -0,0 +1,14 @@
1
+ import { google } from "googleapis";
2
+ import { Auth } from '../../support/auth.js'
3
+ import { syncLog } from '../../support/workersync/synclogger.js'
4
+
5
+ let __client = null;
6
+ syncLog('...importing Calendar API');
7
+ export const getCalendarApiClient = () => {
8
+ const auth = Auth.getAuthClient()
9
+ if (!__client) {
10
+ syncLog('Creating new Calendar API client');
11
+ __client = google.calendar({ version: 'v3', auth });
12
+ }
13
+ return __client;
14
+ }
@@ -18,4 +18,34 @@ class FakeAdvCalendarCalendarList extends FakeAdvResource {
18
18
  this.calendar = mainService;
19
19
  this.__fakeObjectType = "Calendar.CalendarList";
20
20
  }
21
+
22
+ list(options = {}) {
23
+ const { response, data } = this._call("list", options);
24
+ return data;
25
+ }
26
+
27
+ get(calendarId, options) {
28
+ const { response, data } = this._call("get", { calendarId, ...options });
29
+ return data;
30
+ }
31
+
32
+ insert(requestBody, options) {
33
+ const { response, data } = this._call("insert", { requestBody, ...options });
34
+ return data;
35
+ }
36
+
37
+ patch(requestBody, calendarId, options) {
38
+ const { response, data } = this._call("patch", { calendarId, requestBody, ...options });
39
+ return data;
40
+ }
41
+
42
+ update(requestBody, calendarId, options) {
43
+ const { response, data } = this._call("update", { calendarId, requestBody, ...options });
44
+ return data;
45
+ }
46
+
47
+ remove(calendarId, options) {
48
+ const { response, data } = this._call("remove", { calendarId, ...options });
49
+ return data;
50
+ }
21
51
  }
@@ -18,4 +18,29 @@ class FakeAdvCalendarCalendars extends FakeAdvResource {
18
18
  this.calendar = mainService;
19
19
  this.__fakeObjectType = "Calendar.Calendars";
20
20
  }
21
+
22
+ get(calendarId, options) {
23
+ const { response, data } = this._call("get", { calendarId, ...options });
24
+ return data;
25
+ }
26
+
27
+ insert(requestBody, options) {
28
+ const { response, data } = this._call("insert", { requestBody, ...options });
29
+ return data;
30
+ }
31
+
32
+ patch(requestBody, calendarId, options) {
33
+ const { response, data } = this._call("patch", { calendarId, requestBody, ...options });
34
+ return data;
35
+ }
36
+
37
+ update(requestBody, calendarId, options) {
38
+ const { response, data } = this._call("update", { calendarId, requestBody, ...options });
39
+ return data;
40
+ }
41
+
42
+ delete(calendarId, options) {
43
+ const { response, data } = this._call("delete", { calendarId, ...options });
44
+ return data;
45
+ }
21
46
  }
@@ -18,4 +18,54 @@ class FakeAdvCalendarEvents extends FakeAdvResource {
18
18
  this.calendar = mainService;
19
19
  this.__fakeObjectType = "Calendar.Events";
20
20
  }
21
- }
21
+
22
+ list(calendarId, options) {
23
+ const { response, data } = this._call("list", { calendarId, ...options });
24
+ return data;
25
+ }
26
+
27
+ get(calendarId, eventId, options) {
28
+ const { response, data } = this._call("get", { calendarId, eventId, ...options });
29
+ return data;
30
+ }
31
+
32
+ insert(requestBody, calendarId, options) {
33
+ const { response, data } = this._call("insert", { calendarId, requestBody, ...options });
34
+ return data;
35
+ }
36
+
37
+ update(requestBody, calendarId, eventId, options) {
38
+ const { response, data } = this._call("update", { calendarId, eventId, requestBody, ...options });
39
+ return data;
40
+ }
41
+
42
+ patch(requestBody, calendarId, eventId, options) {
43
+ const { response, data } = this._call("patch", { calendarId, eventId, requestBody, ...options });
44
+ return data;
45
+ }
46
+
47
+ delete(calendarId, eventId, options) {
48
+ const { response, data } = this._call("delete", { calendarId, eventId, ...options });
49
+ return data;
50
+ }
51
+
52
+ quickAdd(calendarId, text, options) {
53
+ const { response, data } = this._call("quickAdd", { calendarId, text, ...options });
54
+ return data;
55
+ }
56
+
57
+ move(calendarId, eventId, destinationCalendarId, options) {
58
+ const { response, data } = this._call("move", { calendarId, eventId, destinationCalendarId, ...options });
59
+ return data;
60
+ }
61
+
62
+ import(requestBody, calendarId, options) {
63
+ const { response, data } = this._call("import", { calendarId, requestBody, ...options });
64
+ return data;
65
+ }
66
+
67
+ instances(calendarId, eventId, options) {
68
+ const { response, data } = this._call("instances", { calendarId, eventId, ...options });
69
+ return data;
70
+ }
71
+ }
@@ -0,0 +1,11 @@
1
+
2
+
3
+ /**
4
+ * the idea here is to create an empty global entry for the singleton
5
+ * but only load it when it is actually used.
6
+ */
7
+ import { newFakeCalendarApp as maker } from './fakecalendarapp.js';
8
+ import { lazyLoaderApp } from '../common/lazyloader.js'
9
+
10
+ let _app = null;
11
+ _app = lazyLoaderApp(_app, 'CalendarApp', maker)
@@ -0,0 +1,328 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { newFakeCalendarEvent } from './fakecalendarevent.js';
3
+ import { newFakeCalendarEventSeries } from './fakecalendareventseries.js';
4
+
5
+ export const newFakeCalendar = (...args) => {
6
+ return Proxies.guard(new FakeCalendar(...args));
7
+ };
8
+
9
+ /**
10
+ * Represents a calendar in the CalendarApp service.
11
+ * @see https://developers.google.com/apps-script/reference/calendar/calendar
12
+ */
13
+ export class FakeCalendar {
14
+ /**
15
+ * @param {string} id The calendar ID.
16
+ * @param {object} resource The underlying Calendar resource (Advanced).
17
+ */
18
+ constructor(id, resource) {
19
+ this.__id = id;
20
+ }
21
+
22
+ /**
23
+ * Internal helper to refresh or get the current resource (Calendars).
24
+ */
25
+ get __resource() {
26
+ // calendar caching will will only update if its changed
27
+ return Calendar.Calendars.get(this.__id);
28
+ }
29
+
30
+ /**
31
+ * Internal helper to get the current list entry resource (CalendarList).
32
+ * Used for user-specific properties like color, hidden, selected.
33
+ */
34
+ get __listEntry() {
35
+ try {
36
+ return Calendar.CalendarList.get(this.__id);
37
+ } catch (e) {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ getId() {
43
+ return this.__id || this.__resource.id;
44
+ }
45
+
46
+ getName() {
47
+ // summary can be in list entry (override) or calendar resource
48
+ const entry = this.__listEntry;
49
+ if (entry && entry.summaryOverride) return entry.summaryOverride;
50
+ return this.__resource.summary || '';
51
+ }
52
+
53
+ setName(name) {
54
+ this.__checkWriteAccess();
55
+ // Sets the name of the calendar. This updates the global name (Calendars).
56
+ Calendar.Calendars.patch({ summary: name }, this.getId());
57
+ return this;
58
+ }
59
+
60
+ getDescription() {
61
+ return this.__resource.description || '';
62
+ }
63
+
64
+ setDescription(description) {
65
+ this.__checkWriteAccess();
66
+ Calendar.Calendars.patch({ description }, this.getId());
67
+ return this;
68
+ }
69
+
70
+ getTimeZone() {
71
+ return this.__resource.timeZone || '';
72
+ }
73
+
74
+ setTimeZone(timeZone) {
75
+ this.__checkWriteAccess();
76
+ Calendar.Calendars.patch({ timeZone }, this.getId());
77
+ return this;
78
+ }
79
+
80
+ getColor() {
81
+ const entry = this.__listEntry;
82
+ return entry ? entry.backgroundColor : '';
83
+ }
84
+
85
+ setColor(color) {
86
+ this.__checkWriteAccess();
87
+ Calendar.CalendarList.patch({ backgroundColor: color }, this.getId());
88
+ return this;
89
+ }
90
+
91
+ isHidden() {
92
+ const entry = this.__listEntry;
93
+ return entry ? !!entry.hidden : false;
94
+ }
95
+
96
+ setHidden(hidden) {
97
+ this.__checkWriteAccess();
98
+ Calendar.CalendarList.patch({ hidden }, this.getId());
99
+ return this;
100
+ }
101
+
102
+ isSelected() {
103
+ const entry = this.__listEntry;
104
+ return entry ? !!entry.selected : false;
105
+ }
106
+
107
+ setSelected(selected) {
108
+ this.__checkWriteAccess();
109
+ Calendar.CalendarList.patch({ selected }, this.getId());
110
+ return this;
111
+ }
112
+
113
+ isMyPrimaryCalendar() {
114
+ return this.getId() === 'primary' || (this.__listEntry && this.__listEntry.primary);
115
+ }
116
+
117
+ isOwnedByMe() {
118
+ const entry = this.__listEntry;
119
+ return entry ? entry.accessRole === 'owner' : false;
120
+ }
121
+
122
+ /**
123
+ * Permanently deletes a calendar.
124
+ */
125
+ deleteCalendar() {
126
+ this.__checkDeleteAccess();
127
+ Calendar.Calendars.remove(this.getId());
128
+ }
129
+
130
+ unsubscribeFromCalendar() {
131
+ // Unsubscribes the user from a calendar (removes from list).
132
+ Calendar.CalendarList.remove(this.getId());
133
+ }
134
+
135
+ // --- Events ---
136
+
137
+ createEvent(title, startTime, endTime, options) {
138
+ if (startTime.getTime() >= endTime.getTime()) {
139
+ throw new Error('Event start date must be before event end date.');
140
+ }
141
+ this.__checkWriteAccess();
142
+ const resource = {
143
+ summary: title,
144
+ start: { dateTime: startTime.toISOString() },
145
+ end: { dateTime: endTime.toISOString() }
146
+ };
147
+ this.__applyEventOptions(resource, options);
148
+
149
+ const args = {};
150
+ if (options && options.sendInvites) {
151
+ args.sendUpdates = 'all';
152
+ }
153
+
154
+ const event = Calendar.Events.insert(resource, this.getId(), args);
155
+ return newFakeCalendarEvent(this.getId(), event);
156
+ }
157
+
158
+ createAllDayEvent(title, startDate, endDateOrOptions, options) {
159
+ this.__checkWriteAccess();
160
+
161
+ let endDate = startDate;
162
+ let opts = options;
163
+
164
+ if (endDateOrOptions instanceof Date) {
165
+ endDate = endDateOrOptions;
166
+ } else if (typeof endDateOrOptions === 'object') {
167
+ opts = endDateOrOptions;
168
+ // Single day event, end date should be start date + 1 day for API v3 (exclusive)
169
+ const nextDay = new Date(startDate);
170
+ nextDay.setDate(nextDay.getDate() + 1);
171
+ endDate = nextDay;
172
+ } else {
173
+ // Just title and date
174
+ const nextDay = new Date(startDate);
175
+ nextDay.setDate(nextDay.getDate() + 1);
176
+ endDate = nextDay;
177
+ }
178
+
179
+ if (startDate.getTime() >= endDate.getTime()) {
180
+ throw new Error('Event start date must be before event end date.');
181
+ }
182
+
183
+ const toDateString = (date) => date.toISOString().split('T')[0];
184
+
185
+ const resource = {
186
+ summary: title,
187
+ start: { date: toDateString(startDate) },
188
+ end: { date: toDateString(endDate) }
189
+ };
190
+ this.__applyEventOptions(resource, opts);
191
+
192
+ const args = {};
193
+ if (opts && opts.sendInvites) {
194
+ args.sendUpdates = 'all';
195
+ }
196
+
197
+ const event = Calendar.Events.insert(resource, this.getId(), args);
198
+ return newFakeCalendarEvent(this.getId(), event);
199
+ }
200
+
201
+ createEventFromDescription(description) {
202
+ this.__checkWriteAccess();
203
+ const event = Calendar.Events.quickAdd(this.getId(), description);
204
+ return newFakeCalendarEvent(this.getId(), event);
205
+ }
206
+
207
+ getEvents(startTime, endTime, options) {
208
+ this.__checkReadAccess();
209
+ const args = {
210
+ timeMin: startTime.toISOString(),
211
+ timeMax: endTime.toISOString(),
212
+ singleEvents: true // Expand recurring events usually expected
213
+ };
214
+
215
+ if (options) {
216
+ if (options.start !== undefined) args.startIndex = options.start; // Note: Not in v3 standard list?
217
+ if (options.max !== undefined) args.maxResults = options.max;
218
+ if (options.search) args.q = options.search;
219
+ if (options.author) { /* Not directly supported in v3 list? */ }
220
+ }
221
+
222
+ const list = Calendar.Events.list(this.getId(), args);
223
+ return (list.items || []).map(item => newFakeCalendarEvent(this.getId(), item));
224
+ }
225
+
226
+ getEventsForDay(date, options) {
227
+ const startTime = new Date(date);
228
+ startTime.setHours(0, 0, 0, 0);
229
+ const endTime = new Date(date);
230
+ endTime.setHours(23, 59, 59, 999);
231
+ return this.getEvents(startTime, endTime, options);
232
+ }
233
+
234
+ getEventById(iCalId) {
235
+ this.__checkReadAccess();
236
+ // Apps Script expects iCalUID. Try finding by iCalUID first using list.
237
+ // This avoids "Not Found" errors in the worker when passing iCalUID to events.get (which expects eventId)
238
+ const list = Calendar.Events.list(this.getId(), { iCalUID: iCalId });
239
+ if (list.items && list.items.length > 0) {
240
+ return newFakeCalendarEvent(this.getId(), list.items[0]);
241
+ }
242
+
243
+ // Fallback: Try getting by eventId directly.
244
+ try {
245
+ const event = Calendar.Events.get(this.getId(), iCalId);
246
+ return newFakeCalendarEvent(this.getId(), event);
247
+ } catch (e) {
248
+ return null;
249
+ }
250
+ }
251
+
252
+ // --- Series ---
253
+
254
+ createEventSeries(title, startTime, endTime, recurrence, options) {
255
+ // TODO: Implement recurrence conversion to RRULE
256
+ // For now, simple insert without proper RRULE if Recurrence object not fully implemented
257
+ return this.createEvent(title, startTime, endTime, options); // Fallback
258
+ }
259
+
260
+ createAllDayEventSeries(title, startDate, recurrence, options) {
261
+ // TODO
262
+ return this.createAllDayEvent(title, startDate, options); // Fallback
263
+ }
264
+
265
+ getEventSeriesById(iCalId) {
266
+ // TODO
267
+ return null;
268
+ }
269
+
270
+ // --- Internal ---
271
+
272
+ __applyEventOptions(resource, options) {
273
+ if (options) {
274
+ if (options.description) resource.description = options.description;
275
+ if (options.location) resource.location = options.location;
276
+ if (options.guests) resource.attendees = options.guests.split(',').map(e => ({ email: e.trim() }));
277
+ }
278
+ }
279
+
280
+ __checkAccess(accessType) {
281
+ const behavior = ScriptApp.__behavior;
282
+ if (!behavior.sandboxMode) return true;
283
+
284
+ const calendarId = this.getId();
285
+
286
+ // Session-created calendars are always writable
287
+ if (behavior.isKnownCalendar(calendarId)) return true;
288
+
289
+ // Check whitelist
290
+ const settings = behavior.sandboxService.CalendarApp;
291
+ const whitelist = settings && settings.calendarWhitelist;
292
+
293
+ if (!whitelist) {
294
+ throw new Error(`Access to calendar ${calendarId} is denied. No calendar whitelist configured.`);
295
+ }
296
+
297
+ const calendarName = this.getName();
298
+ // primary check
299
+ if (calendarId === 'primary') {
300
+ const entry = whitelist.find(item => item.name === 'Primary' || item.name === 'primary');
301
+ if (entry && entry[accessType]) return true;
302
+ // Default primary to accessible if not explicit? Or strictly follow whitelist?
303
+ // Existing code had "Primary calendar is always accessible" for *read* in CalendarApp.
304
+ // But here we check specific access.
305
+ }
306
+
307
+ const entry = whitelist.find(item => item.name === calendarName);
308
+ if (entry && entry[accessType]) {
309
+ return true;
310
+ }
311
+
312
+ throw new Error(`${accessType} access to calendar "${calendarName}" (${calendarId}) is denied by sandbox rules`);
313
+ }
314
+
315
+ __checkDeleteAccess() {
316
+ return this.__checkAccess('write'); // Delete requires write usually? Or 'delete'?
317
+ }
318
+ __checkWriteAccess() {
319
+ return this.__checkAccess('write');
320
+ }
321
+ __checkReadAccess() {
322
+ return this.__checkAccess('read');
323
+ }
324
+
325
+ toString() {
326
+ return 'Calendar';
327
+ }
328
+ }
@@ -0,0 +1,147 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import * as CalendarEnums from '../enums/calendarenums.js';
3
+ import { newFakeCalendar } from './fakecalendar.js';
4
+
5
+ /**
6
+ * Creates a new FakeCalendarApp instance.
7
+ * @returns {FakeCalendarApp} The new instance.
8
+ */
9
+ export const newFakeCalendarApp = () => {
10
+ return Proxies.guard(new FakeCalendarApp());
11
+ };
12
+
13
+ /**
14
+ * Placeholder for CalendarApp service.
15
+ * @see https://developers.google.com/apps-script/reference/calendar/calendar-app
16
+ */
17
+ export class FakeCalendarApp {
18
+ constructor() {
19
+ // Attach enums
20
+ Object.assign(this, CalendarEnums);
21
+ }
22
+
23
+ createCalendar(name, options = {}) {
24
+ ScriptApp.__behavior.checkMethod('CalendarApp', 'createCalendar');
25
+ this.__checkUsage('write');
26
+ const resource = Calendar.Calendars.insert({ summary: name, ...options });
27
+ if (ScriptApp.__behavior.sandboxMode && resource.id) {
28
+ ScriptApp.__behavior.addCalendarId(resource.id);
29
+ }
30
+ return newFakeCalendar(resource.id, resource);
31
+ }
32
+
33
+ getCalendarById(id) {
34
+ ScriptApp.__behavior.checkMethod('CalendarApp', 'getCalendarById');
35
+ this.__checkUsage('read');
36
+ this.__checkCalendarAccess(id);
37
+ const resource = Calendar.Calendars.get(id);
38
+ return newFakeCalendar(id, resource);
39
+ }
40
+
41
+ getDefaultCalendar() {
42
+ ScriptApp.__behavior.checkMethod('CalendarApp', 'getDefaultCalendar');
43
+ return this.getCalendarById('primary');
44
+ }
45
+
46
+ getCalendarsByName(name) {
47
+ ScriptApp.__behavior.checkMethod('CalendarApp', 'getCalendarsByName');
48
+ this.__checkUsage('read');
49
+ const list = Calendar.CalendarList.list();
50
+ return (list.items || [])
51
+ .filter(item => this.__isCalendarAccessible(item.id))
52
+ .filter(item => item.summary === name)
53
+ .map(item => newFakeCalendar(item.id, item));
54
+ }
55
+
56
+ getAllCalendars() {
57
+ ScriptApp.__behavior.checkMethod('CalendarApp', 'getAllCalendars');
58
+ this.__checkUsage('read');
59
+ const list = Calendar.CalendarList.list();
60
+ return (list.items || [])
61
+ .filter(item => this.__isCalendarAccessible(item.id))
62
+ .map(item => newFakeCalendar(item.id, item));
63
+ }
64
+
65
+ getAllOwnedCalendars() {
66
+ ScriptApp.__behavior.checkMethod('CalendarApp', 'getAllOwnedCalendars');
67
+ this.__checkUsage('read');
68
+ const list = Calendar.CalendarList.list();
69
+ return (list.items || [])
70
+ .filter(item => this.__isCalendarAccessible(item.id))
71
+ .filter(item => item.accessRole === 'owner')
72
+ .map(item => newFakeCalendar(item.id, item));
73
+ }
74
+
75
+ __isCalendarAccessible(calendarId) {
76
+ try {
77
+ this.__checkCalendarAccess(calendarId);
78
+ return true;
79
+ } catch (e) {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ __checkCalendarAccess(calendarId) {
85
+ const behavior = ScriptApp.__behavior;
86
+ if (!behavior.sandboxMode) return true;
87
+
88
+ // 1. Session check - calendars created in this session are always accessible
89
+ if (behavior.isKnownCalendar(calendarId)) return true;
90
+
91
+ // 2. Primary calendar is always accessible
92
+ if (calendarId === 'primary') return true;
93
+
94
+ // 3. Whitelist check
95
+ const settings = behavior.sandboxService.CalendarApp;
96
+ const whitelist = settings && settings.calendarWhitelist;
97
+
98
+ if (!whitelist) {
99
+ throw new Error(`Access to calendar ${calendarId} is denied. No calendar whitelist configured.`);
100
+ }
101
+
102
+ // Get calendar details to check name
103
+ const calendar = Calendar.Calendars.get(calendarId);
104
+ const calendarName = calendar.summary;
105
+
106
+ // Check if calendar name is in whitelist with read permission
107
+ const entry = whitelist.find(item => item.name === calendarName);
108
+ if (entry && entry.read) {
109
+ return true;
110
+ }
111
+
112
+ throw new Error(`Access to calendar "${calendarName}" (${calendarId}) is denied by sandbox rules`);
113
+ }
114
+
115
+ __checkUsage(type) {
116
+ const serviceName = 'CalendarApp';
117
+ const behavior = ScriptApp.__behavior;
118
+ if (behavior.sandboxMode) {
119
+ const settings = behavior.sandboxService[serviceName];
120
+ let limit = settings && settings.usageLimit;
121
+ if (limit) {
122
+ if (typeof limit === 'number') {
123
+ const total = (settings.usageCount.read || 0) + (settings.usageCount.write || 0) + (settings.usageCount.trash || 0);
124
+ if (total >= limit) {
125
+ throw new Error(`Calendar total usage limit of ${limit} exceeded`);
126
+ }
127
+ settings.incrementUsage(type);
128
+ return;
129
+ }
130
+
131
+ let specificLimit = limit[type];
132
+ if (specificLimit !== undefined) {
133
+ const current = settings.usageCount[type] || 0;
134
+ if (current >= specificLimit) {
135
+ throw new Error(`Calendar ${type} usage limit of ${specificLimit} exceeded`);
136
+ }
137
+ settings.incrementUsage(type);
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ // Common pattern in gas-fakes for internal use
144
+ __addAllowed(id) {
145
+ // Shared logic with other services to track created resources
146
+ }
147
+ }
@@ -0,0 +1,44 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+
3
+ export const newFakeCalendarEvent = (...args) => {
4
+ return Proxies.guard(new FakeCalendarEvent(...args));
5
+ };
6
+
7
+ /**
8
+ * Represents a calendar event.
9
+ * @see https://developers.google.com/apps-script/reference/calendar/calendar-event
10
+ */
11
+ export class FakeCalendarEvent {
12
+ /**
13
+ * @param {string} calendarId The calendar ID.
14
+ * @param {object} resource The underlying Event resource (Advanced).
15
+ */
16
+ constructor(calendarId, resource) {
17
+ this.__calendarId = calendarId;
18
+ this.__id = resource.id;
19
+ this.__iCalUID = resource.iCalUID;
20
+ }
21
+
22
+ get __resource() {
23
+ // Try to get by ID
24
+ try {
25
+ return Calendar.Events.get(this.__calendarId, this.__id);
26
+ } catch (e) {
27
+ // Fallback: search by iCalUID if main ID fails (though usually id is reliable for get)
28
+ // or if the event was deleted?
29
+ throw new Error(`Event with ID ${this.__id} not found in calendar ${this.__calendarId}`);
30
+ }
31
+ }
32
+
33
+ getId() {
34
+ return this.__resource.iCalUID || this.__resource.id;
35
+ }
36
+
37
+ getTitle() {
38
+ return this.__resource.summary || '';
39
+ }
40
+
41
+ toString() {
42
+ return 'CalendarEvent';
43
+ }
44
+ }
@@ -0,0 +1,36 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+
3
+ export const newFakeCalendarEventSeries = (...args) => {
4
+ return Proxies.guard(new FakeCalendarEventSeries(...args));
5
+ };
6
+
7
+ /**
8
+ * Represents a series of events (a recurring event).
9
+ * @see https://developers.google.com/apps-script/reference/calendar/calendar-event-series
10
+ */
11
+ export class FakeCalendarEventSeries {
12
+ /**
13
+ * @param {string} calendarId The calendar ID.
14
+ * @param {object} resource The underlying Event resource (Advanced).
15
+ */
16
+ constructor(calendarId, resource) {
17
+ this.__calendarId = calendarId;
18
+ this.__id = resource.id;
19
+ }
20
+
21
+ get __resource() {
22
+ try {
23
+ return Calendar.Events.get(this.__calendarId, this.__id);
24
+ } catch (e) {
25
+ throw new Error(`Event Series with ID ${this.__id} not found in calendar ${this.__calendarId}`);
26
+ }
27
+ }
28
+
29
+ getId() {
30
+ return this.__resource.iCalUID || this.__resource.id;
31
+ }
32
+
33
+ toString() {
34
+ return 'CalendarEventSeries';
35
+ }
36
+ }
@@ -0,0 +1,19 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+
3
+ export const newFakeEventRecurrence = (...args) => {
4
+ return Proxies.guard(new FakeEventRecurrence(...args));
5
+ };
6
+
7
+ /**
8
+ * Represents the recurrence settings for an event series.
9
+ * @see https://developers.google.com/apps-script/reference/calendar/event-recurrence
10
+ */
11
+ export class FakeEventRecurrence {
12
+ constructor() {
13
+ // TODO: Implement recurrence rules storage
14
+ }
15
+
16
+ toString() {
17
+ return 'EventRecurrence';
18
+ }
19
+ }
@@ -0,0 +1,85 @@
1
+ import { newFakeGasenum} from "@mcpher/fake-gasenum";
2
+ export const Color = newFakeGasenum([
3
+ "BLUE",
4
+ "BROWN",
5
+ "CHARCOAL",
6
+ "CHESTNUT",
7
+ "GRAY",
8
+ "GREEN",
9
+ "INDIGO",
10
+ "LIME",
11
+ "MUSTARD",
12
+ "OLIVE",
13
+ "ORANGE",
14
+ "PINK",
15
+ "PLUM",
16
+ "PURPLE",
17
+ "RED",
18
+ "RED_ORANGE",
19
+ "SEA_BLUE",
20
+ "SLATE",
21
+ "TEAL",
22
+ "TURQOISE",
23
+ "YELLOW"
24
+ ])
25
+ export const EventColor = newFakeGasenum([
26
+ "BLUE",
27
+ "CYAN",
28
+ "GRAY",
29
+ "GREEN",
30
+ "MAUVE",
31
+ "ORANGE",
32
+ "PALE_BLUE",
33
+ "PALE_GREEN",
34
+ "PALE_RED",
35
+ "RED",
36
+ "YELLOW"
37
+ ])
38
+ export const EventTransparency = newFakeGasenum([
39
+ "OPAQUE",
40
+ "TRANSPARENT"
41
+ ])
42
+ export const EventType = newFakeGasenum([
43
+ "DEFAULT",
44
+ "BIRTHDAY",
45
+ "FOCUS_TIME",
46
+ "FROM_GMAIL",
47
+ "OUT_OF_OFFICE",
48
+ "WORKING_LOCATION"
49
+ ])
50
+ export const GuestStatus = newFakeGasenum([
51
+ "INVITED",
52
+ "MAYBE",
53
+ "NO",
54
+ "OWNER",
55
+ "YES"
56
+ ])
57
+ export const Month = newFakeGasenum([
58
+ "JANUARY",
59
+ "FEBRUARY",
60
+ "MARCH",
61
+ "APRIL",
62
+ "MAY",
63
+ "JUNE",
64
+ "JULY",
65
+ "AUGUST",
66
+ "SEPTEMBER",
67
+ "OCTOBER",
68
+ "NOVEMBER",
69
+ "DECEMBER"
70
+ ])
71
+ export const Visibility = newFakeGasenum([
72
+ "CONFIDENTIAL",
73
+ "DEFAULT",
74
+ "PRIVATE",
75
+ "PUBLIC"
76
+ ])
77
+ export const Weekday = newFakeGasenum([
78
+ "SUNDAY",
79
+ "MONDAY",
80
+ "TUESDAY",
81
+ "WEDNESDAY",
82
+ "THURSDAY",
83
+ "FRIDAY",
84
+ "SATURDAY"
85
+ ])
@@ -17,6 +17,7 @@ const serviceModel = {
17
17
  methodWhitelist: null,
18
18
  emailWhitelist: null,
19
19
  labelWhitelist: null,
20
+ calendarWhitelist: null,
20
21
  usageLimit: null,
21
22
  usageCount: 0
22
23
  }
@@ -169,6 +170,17 @@ class FakeSandboxService {
169
170
  return this.__state.labelWhitelist
170
171
  }
171
172
 
173
+ set calendarWhitelist(value) {
174
+ if (!is.null(value)) {
175
+ checkArgs(value, "array")
176
+ // We expect objects like { name: 'calendar-name', read?: boolean, write?: boolean, delete?: boolean }
177
+ }
178
+ this.__state.calendarWhitelist = value
179
+ }
180
+ get calendarWhitelist() {
181
+ return this.__state.calendarWhitelist
182
+ }
183
+
172
184
  set usageLimit(value) {
173
185
  if (!is.null(value)) {
174
186
  // expect object with read, write, trash keys optionally
@@ -211,6 +223,11 @@ class FakeSandboxService {
211
223
  return this.__state.usageCount[type];
212
224
  }
213
225
 
226
+ resetUsageCount() {
227
+ this.__state.usageCount = { read: 0, write: 0, trash: 0, send: 0 };
228
+ return this;
229
+ }
230
+
214
231
  set enabled(value) {
215
232
  this.__state.enabled = checkArgs(value)
216
233
  }
@@ -236,6 +253,7 @@ class FakeBehavior {
236
253
  // key is the file id
237
254
  this.__createdIds = new Set();
238
255
  this.__createdGmailIds = new Set();
256
+ this.__createdCalendarIds = new Set();
239
257
  // in sandbox mode we only allow access to files created in this instance
240
258
  // this is to emulate the behavior of a drive.file scope
241
259
  this.__sandboxMode = false;
@@ -347,6 +365,18 @@ class FakeBehavior {
347
365
  }
348
366
  return id
349
367
  }
368
+ addCalendarId(id) {
369
+ if (this.sandboxMode) {
370
+ if (!is.nonEmptyString(id)) {
371
+ throw new Error(`Invalid sandbox id parameter (${id}) - must be a non-empty string`);
372
+ }
373
+ if (!this.isKnownCalendar(id)) {
374
+ slogger.log(`...adding calendar id ${id} to sandbox allowed list`);
375
+ this.__createdCalendarIds.add(id);
376
+ }
377
+ }
378
+ return id
379
+ }
350
380
  isAccessible(id, serviceName, accessType = 'read') {
351
381
  if (this.sandboxMode && !is.nonEmptyString(id)) {
352
382
  throw new Error(`Invalid sandbox id parameter (${id}) - must be a non-empty string`);
@@ -510,7 +540,29 @@ class FakeBehavior {
510
540
  slogger.log('...skipping cleaning up sandbox files (Gmail)');
511
541
  }
512
542
 
513
- slogger.log(`...trashed ${trashed.length} sandboxed files and ${trashedGmail.length} gmail items`);
543
+ // Clean up Calendar artifacts
544
+ let trashedCalendars = [];
545
+ const calendarSettings = this.sandboxService.CalendarApp;
546
+ const calendarCleanup = calendarSettings && calendarSettings.cleanup;
547
+
548
+ if (calendarCleanup) {
549
+ trashedCalendars = Array.from(this.__createdCalendarIds).reduce((acc, id) => {
550
+ try {
551
+ // Delete calendar
552
+ Calendar.Calendars.delete(id);
553
+ slogger.log(`...deleted calendar ${id}`);
554
+ acc.push(id);
555
+ } catch (e) {
556
+ slogger.log(`...failed to delete calendar ${id}: ${e.message}`);
557
+ }
558
+ return acc;
559
+ }, []);
560
+ this.__createdCalendarIds.clear();
561
+ } else {
562
+ slogger.log('...skipping cleaning up sandbox calendars');
563
+ }
564
+
565
+ slogger.log(`...trashed ${trashed.length} sandboxed files, ${trashedGmail.length} gmail items, and ${trashedCalendars.length} calendars`);
514
566
  return trashed;
515
567
  }
516
568
  isKnown(id) {
@@ -519,4 +571,7 @@ class FakeBehavior {
519
571
  isKnownGmail(id) {
520
572
  return this.__createdGmailIds.has(id);
521
573
  }
574
+ isKnownCalendar(id) {
575
+ return this.__createdCalendarIds.has(id);
576
+ }
522
577
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * CALENDAR
3
+ * all these functions run in the worker
4
+ * thus turning async operations into sync
5
+ * note
6
+ * - arguments and returns must be serializable ie. primitives or plain objects
7
+ */
8
+
9
+ import { responseSyncify } from './auth.js';
10
+ import { syncWarn, syncError } from './workersync/synclogger.js';
11
+ import { getCalendarApiClient } from '../services/advcalendar/clapis.js';
12
+
13
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
14
+
15
+ /**
16
+ * sync a call to calendar api
17
+ * @param {object} Auth the auth object
18
+ * @param {object} p pargs
19
+ * @param {string} p.prop the prop of calendar eg 'calendars' for calendar.calendars
20
+ * @param {string} p.method the method of calendar eg 'get' for calendar.calendars.get
21
+ * @param {object} p.params the params to add to the request
22
+ * @param {object} p.options gaxios options
23
+ * @return {import('./sxdrive.js').SxResult} from the Calendar api
24
+ */
25
+ export const sxCalendar = async (Auth, { prop, method, params, options = {} }) => {
26
+
27
+ const apiClient = getCalendarApiClient();
28
+
29
+ const maxRetries = 7;
30
+ let delay = 1777;
31
+
32
+ for (let i = 0; i < maxRetries; i++) {
33
+ let response;
34
+ let error;
35
+
36
+ try {
37
+ const callish = apiClient[prop];
38
+ response = await callish[method](params, options);
39
+ } catch (err) {
40
+ error = err;
41
+ response = err.response;
42
+ }
43
+
44
+ const isRetryable = [429, 500, 503].includes(response?.status) || error?.code == 429;
45
+
46
+ if (isRetryable && i < maxRetries - 1) {
47
+ // add a random jitter to avoid thundering herd
48
+ const jitter = Math.floor(Math.random() * 1000);
49
+ syncWarn(`Retryable error on Calendar API call ${prop}.${method} (status: ${response?.status}). Retrying in ${delay + jitter}ms...`);
50
+ await sleep(delay + jitter);
51
+ delay *= 2;
52
+ continue;
53
+ }
54
+
55
+ if (error || isRetryable) {
56
+ syncError(`Failed in sxCalendar for ${prop}.${method}`, error);
57
+ return { data: null, response: responseSyncify(response) };
58
+ }
59
+ return { data: response.data, response: responseSyncify(response) };
60
+ }
61
+ };
@@ -17,6 +17,7 @@ import { gmailCacher } from "./gmailcacher.js";
17
17
  import { formsCacher } from "./formscacher.js";
18
18
  import { slidesCacher } from "./slidescacher.js";
19
19
  import { sheetsCacher } from "./sheetscacher.js";
20
+ import { calendarCacher } from "./calendarcacher.js";
20
21
  import is from "@sindresorhus/is";
21
22
  import { callSync } from "./workersync/synchronizer.js";
22
23
 
@@ -401,6 +402,13 @@ const fxGmail = (args) =>
401
402
  cacher: gmailCacher,
402
403
  idField: "id",
403
404
  });
405
+ const fxCalendar = (args) =>
406
+ fxGeneric({
407
+ ...args,
408
+ serviceName: "Calendar",
409
+ cacher: calendarCacher,
410
+ idField: "calendarId",
411
+ });
404
412
 
405
413
  // const fxGetImagesFromXlsx = (args) => callSync("sxGetImagesFromXlsx", args);
406
414
 
@@ -421,5 +429,6 @@ export const Syncit = {
421
429
  fxDocs,
422
430
  fxForms,
423
431
  fxGmail,
432
+ fxCalendar,
424
433
  fxDriveExport
425
434
  }
@@ -8,6 +8,7 @@ export * from "../sxstore.js";
8
8
  export * from "../sxzip.js";
9
9
  export * from "../sxauth.js";
10
10
  export * from "../sxgmail.js";
11
+ export * from "../sxcalendar.js";
11
12
  export * from "../sxxlsx.js";
12
13
 
13
14