@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 +1 -1
- package/src/cli/setup.js +36 -7
- package/src/cli/utils.js +6 -3
- package/src/index.js +1 -0
- package/src/services/advcalendar/clapis.js +14 -0
- package/src/services/advcalendar/fakeadvcalendarcalendarlist.js +30 -0
- package/src/services/advcalendar/fakeadvcalendarcalendars.js +25 -0
- package/src/services/advcalendar/fakeadvcalendarevents.js +51 -1
- package/src/services/calendarapp/app.js +11 -0
- package/src/services/calendarapp/fakecalendar.js +328 -0
- package/src/services/calendarapp/fakecalendarapp.js +147 -0
- package/src/services/calendarapp/fakecalendarevent.js +44 -0
- package/src/services/calendarapp/fakecalendareventseries.js +36 -0
- package/src/services/calendarapp/fakeeventrecurrence.js +19 -0
- package/src/services/enums/calendarenums.js +85 -0
- package/src/services/scriptapp/behavior.js +56 -1
- package/src/support/sxcalendar.js +61 -0
- package/src/support/syncit.js +9 -0
- package/src/support/workersync/sxfunctions.js +1 -0
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/src/support/syncit.js
CHANGED
|
@@ -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
|
}
|