@mcpher/gas-fakes 1.2.26 → 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.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/src/cli/setup.js +35 -6
  3. package/src/cli/utils.js +8 -7
  4. package/src/index.js +1 -0
  5. package/src/services/advcalendar/clapis.js +14 -0
  6. package/src/services/advcalendar/fakeadvcalendarcalendarlist.js +30 -0
  7. package/src/services/advcalendar/fakeadvcalendarcalendars.js +25 -0
  8. package/src/services/advcalendar/fakeadvcalendarevents.js +51 -1
  9. package/src/services/calendarapp/app.js +11 -0
  10. package/src/services/calendarapp/fakecalendar.js +328 -0
  11. package/src/services/calendarapp/fakecalendarapp.js +147 -0
  12. package/src/services/calendarapp/fakecalendarevent.js +44 -0
  13. package/src/services/calendarapp/fakecalendareventseries.js +36 -0
  14. package/src/services/calendarapp/fakeeventrecurrence.js +19 -0
  15. package/src/services/enums/calendarenums.js +85 -0
  16. package/src/services/enums/slidesenums.js +1 -1
  17. package/src/services/formapp/fakechoiceitem.js +1 -1
  18. package/src/services/formapp/fakeform.js +4 -3
  19. package/src/services/formapp/fakeformitem.js +1 -2
  20. package/src/services/formapp/fakeformresponse.js +3 -2
  21. package/src/services/formapp/fakelistitem.js +3 -2
  22. package/src/services/scriptapp/behavior.js +56 -1
  23. package/src/services/slidesapp/fakeaffinetransform.js +55 -0
  24. package/src/services/slidesapp/fakeaffinetransformbuilder.js +64 -0
  25. package/src/services/slidesapp/fakeautofit.js +49 -0
  26. package/src/services/slidesapp/fakeautotext.js +29 -0
  27. package/src/services/slidesapp/fakeconnectionsite.js +24 -0
  28. package/src/services/slidesapp/fakelayout.js +38 -0
  29. package/src/services/slidesapp/fakeline.js +195 -0
  30. package/src/services/slidesapp/fakelinefill.js +58 -0
  31. package/src/services/slidesapp/fakelink.js +31 -0
  32. package/src/services/slidesapp/fakemaster.js +28 -0
  33. package/src/services/slidesapp/fakenotespage.js +19 -0
  34. package/src/services/slidesapp/fakepagebackground.js +17 -0
  35. package/src/services/slidesapp/fakepageelement.js +395 -0
  36. package/src/services/slidesapp/fakeparagraph.js +46 -0
  37. package/src/services/slidesapp/fakepoint.js +24 -0
  38. package/src/services/slidesapp/fakepresentation.js +61 -3
  39. package/src/services/slidesapp/fakeshape.js +49 -0
  40. package/src/services/slidesapp/fakeslide.js +232 -0
  41. package/src/services/slidesapp/fakeslidesapp.js +5 -0
  42. package/src/services/slidesapp/faketextrange.js +253 -0
  43. package/src/services/slidesapp/pageelementfactory.js +19 -0
  44. package/src/services/stores/gasflex.js +1 -1
  45. package/src/services/urlfetchapp/app.js +17 -1
  46. package/src/support/sxcalendar.js +61 -0
  47. package/src/support/sxfetch.js +39 -13
  48. package/src/support/sxforms.js +3 -1
  49. package/src/support/syncit.js +16 -2
  50. package/src/support/utils.js +2 -0
  51. package/src/support/workersync/sxfunctions.js +1 -0
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.26",
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",
@@ -408,9 +413,9 @@ export async function initializeConfiguration(options = {}) {
408
413
  /**
409
414
  * Handles the 'auth' command to authenticate with Google Cloud.
410
415
  */
411
- export function authenticateUser() {
416
+ export async function authenticateUser() {
412
417
  // First, check if gcloud CLI is available.
413
- checkForGcloudCli();
418
+ await checkForGcloudCli();
414
419
 
415
420
  const rootDirectory = process.cwd();
416
421
  const envPath = path.join(rootDirectory, ".env");
@@ -486,13 +491,14 @@ export 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 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 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 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 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
@@ -1,11 +1,10 @@
1
- import { spawn } from "child_process";
1
+ import { spawn, execSync } from "child_process";
2
2
  import { createRequire } from "node:module";
3
-
4
3
  const require = createRequire(import.meta.url);
5
4
  const pjson = require("../../package.json");
6
5
 
7
6
  export const VERSION = pjson.version;
8
- export const CLI_VERSION = "0.0.18"; // Kept from original logic
7
+ export const CLI_VERSION = "0.0.19";
9
8
  export const MCP_VERSION = "0.0.7";
10
9
 
11
10
  /**
@@ -26,7 +25,8 @@ export function normalizeScriptNewlines(text) {
26
25
  */
27
26
  export function spawnCommand(command, args) {
28
27
  return new Promise((resolve, reject) => {
29
- 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 });
30
30
  let stdout = "";
31
31
  let stderr = "";
32
32
 
@@ -73,10 +73,11 @@ export async function checkForGcloudCli() {
73
73
  /**
74
74
  * Helper function to run a shell command sync (used in setup).
75
75
  */
76
- export async function runCommandSync(command) {
77
- const { execSync } = await import("child_process");
76
+ export function runCommandSync(command) {
78
77
  try {
79
- 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 });
80
81
  } catch (error) {
81
82
  console.error(`\nError executing command: ${command}`);
82
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
+ }