@jskit-ai/connector-google-calendar 0.1.62 → 0.1.63
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/connector-google-calendar",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
4
4
|
"description": "Google Calendar account configuration and event operations for JSKIT connectors.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"./server": "./src/server/provider.js"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@jskit-ai/connectors-core": "0.1.
|
|
14
|
+
"@jskit-ai/connectors-core": "0.1.63",
|
|
15
15
|
"json-rest-schema": "^1.0.17"
|
|
16
16
|
},
|
|
17
17
|
"jskit": {
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@jskit-ai/kernel": "0.1.
|
|
33
|
+
"@jskit-ai/kernel": "0.1.246"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@jskit-ai/connectors-core": "0.1.
|
|
11
|
-
"@jskit-ai/connector-google-calendar": "0.1.
|
|
12
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/database-runtime-mysql": "0.1.
|
|
10
|
+
"@jskit-ai/connectors-core": "0.1.63",
|
|
11
|
+
"@jskit-ai/connector-google-calendar": "0.1.63",
|
|
12
|
+
"@jskit-ai/database-runtime": "0.1.246",
|
|
13
|
+
"@jskit-ai/database-runtime-mysql": "0.1.244",
|
|
14
14
|
"knex": "^3.1.0"
|
|
15
15
|
}
|
|
16
16
|
}
|
package/src/server/provider.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { createSchema } from "json-rest-schema";
|
|
2
2
|
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
3
|
-
import {
|
|
3
|
+
import { ConnectorError } from "@jskit-ai/connectors-core/server";
|
|
4
|
+
import { googleCalendarDefinition, googleScope } from "../shared/definition.js";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const calendarIdField = { type: "string", minLength: 1, maxLength: 1024, defaultTo: "primary" };
|
|
6
7
|
const listSchema = createSchema({
|
|
7
8
|
pageToken: { type: "string", maxLength: 4096 },
|
|
8
9
|
maxResults: { type: "integer", min: 1, max: 250, defaultTo: 100 }
|
|
9
10
|
});
|
|
10
11
|
const eventsSchema = createSchema({
|
|
11
|
-
calendarId:
|
|
12
|
+
calendarId: calendarIdField,
|
|
12
13
|
pageToken: { type: "string", maxLength: 4096 },
|
|
13
14
|
maxResults: { type: "integer", min: 1, max: 2500, defaultTo: 100 },
|
|
14
15
|
timeMin: { type: "dateTime" },
|
|
@@ -21,13 +22,48 @@ function queryUrl(path, input) {
|
|
|
21
22
|
return { method: "GET", url: url.href };
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const writeScopes = ["calendar.events", "calendar", "calendar.events.owned", "calendar.app.created"].map(
|
|
25
|
-
const readScopes = ["calendar.events.readonly", "calendar.events", "calendar.readonly", "calendar", "calendar.events.owned", "calendar.events.owned.readonly", "calendar.app.created"].map(
|
|
25
|
+
const writeScopes = ["calendar.events", "calendar", "calendar.events.owned", "calendar.app.created"].map(googleScope);
|
|
26
|
+
const readScopes = ["calendar.events.readonly", "calendar.events", "calendar.readonly", "calendar", "calendar.events.owned", "calendar.events.owned.readonly", "calendar.app.created"].map(googleScope);
|
|
27
|
+
const calendarListScopes = ["calendar.calendarlist.readonly", "calendar.calendarlist", "calendar.readonly", "calendar"].map(googleScope);
|
|
28
|
+
const eventListScopes = ["calendar.events.readonly", "calendar.events", "calendar.readonly", "calendar"].map(googleScope);
|
|
29
|
+
const calendarSchema = createSchema({ calendarId: calendarIdField });
|
|
26
30
|
const targetSchema = createSchema({
|
|
27
|
-
calendarId:
|
|
31
|
+
calendarId: calendarIdField,
|
|
28
32
|
eventId: { type: "string", minLength: 1, maxLength: 1024, required: true }
|
|
29
33
|
});
|
|
30
|
-
function invalid(message) { throw
|
|
34
|
+
function invalid(message) { throw new ConnectorError("connector_input_invalid", message, { statusCode: 422 }); }
|
|
35
|
+
function validateEventTime(point) {
|
|
36
|
+
if (typeof point !== "object" || Array.isArray(point) || Object.keys(point).some(key => !["date", "dateTime", "timeZone"].includes(key))) invalid("Use date or dateTime and optional timeZone.");
|
|
37
|
+
if (Boolean(point.date) === Boolean(point.dateTime)) invalid("Choose an all-day date or a timed dateTime.");
|
|
38
|
+
if (point.date && (typeof point.date !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(point.date) ||
|
|
39
|
+
!Number.isFinite(Date.parse(point.date)) || new Date(point.date).toISOString().slice(0, 10) !== point.date)) invalid("Invalid all-day date.");
|
|
40
|
+
if (point.dateTime && (typeof point.dateTime !== "string" || !/T.*(?:Z|[+-]\d{2}:\d{2})$/.test(point.dateTime) ||
|
|
41
|
+
!Number.isFinite(Date.parse(point.dateTime)))) invalid("dateTime requires an explicit timezone offset.");
|
|
42
|
+
if (point.timeZone !== undefined) {
|
|
43
|
+
try {
|
|
44
|
+
if (typeof point.timeZone !== "string") throw new Error();
|
|
45
|
+
new Intl.DateTimeFormat("en", { timeZone: point.timeZone });
|
|
46
|
+
} catch { invalid("Use an IANA timeZone."); }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function validateRecurrence(body) {
|
|
50
|
+
const { recurrence } = body;
|
|
51
|
+
if (!Array.isArray(recurrence) || recurrence.length > 100 || recurrence.some(line => typeof line !== "string" ||
|
|
52
|
+
line.length > 2048 || !/^(RRULE|RDATE|EXRULE|EXDATE)[:;]/.test(line) || /[\r\n]/.test(line))) invalid("recurrence must be an array of RFC5545 recurrence lines.");
|
|
53
|
+
if (recurrence.length && body.start?.dateTime && (!body.start.timeZone || !body.end.timeZone)) invalid("Timed recurrence requires start/end timeZone.");
|
|
54
|
+
if (recurrence.length && !body.start) invalid("Supply start and end when setting recurrence.");
|
|
55
|
+
}
|
|
56
|
+
function validAttendee(person) {
|
|
57
|
+
return person && typeof person.email === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(person.email) &&
|
|
58
|
+
Object.keys(person).every(key => ["email", "displayName", "optional"].includes(key)) &&
|
|
59
|
+
(person.displayName === undefined || typeof person.displayName === "string") &&
|
|
60
|
+
(person.optional === undefined || typeof person.optional === "boolean");
|
|
61
|
+
}
|
|
62
|
+
function validateAttendees(attendees) {
|
|
63
|
+
if (!Array.isArray(attendees) || attendees.length > 200 || !attendees.every(validAttendee)) {
|
|
64
|
+
invalid("attendees requires up to 200 email entries with optional displayName/optional.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
31
67
|
function eventBody(body, creating) {
|
|
32
68
|
if (!body || typeof body !== "object" || Array.isArray(body)) invalid("event must be an object.");
|
|
33
69
|
const allowed = ["summary", "description", "location", "start", "end", "recurrence", "attendees", "transparency", "visibility"];
|
|
@@ -35,20 +71,10 @@ function eventBody(body, creating) {
|
|
|
35
71
|
for (const key of ["summary", "description", "location"]) if (body[key] !== undefined && (typeof body[key] !== "string" || body[key].length > 16384)) invalid(`${key} must be text of at most 16384 characters.`);
|
|
36
72
|
if (creating && (!body.start || !body.end)) invalid("start and end are required.");
|
|
37
73
|
if (Boolean(body.start) !== Boolean(body.end)) invalid("Supply start and end together when changing event times.");
|
|
38
|
-
for (const point of [body.start, body.end].filter(Boolean))
|
|
39
|
-
if (typeof point !== "object" || Array.isArray(point) || Object.keys(point).some(key => !["date", "dateTime", "timeZone"].includes(key))) invalid("Use date or dateTime and optional timeZone.");
|
|
40
|
-
if (Boolean(point.date) === Boolean(point.dateTime)) invalid("Choose an all-day date or a timed dateTime.");
|
|
41
|
-
if (point.date && (typeof point.date !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(point.date) || !Number.isFinite(Date.parse(point.date)) || new Date(point.date).toISOString().slice(0, 10) !== point.date)) invalid("Invalid all-day date.");
|
|
42
|
-
if (point.dateTime && (typeof point.dateTime !== "string" || !/T.*(?:Z|[+-]\d{2}:\d{2})$/.test(point.dateTime) || !Number.isFinite(Date.parse(point.dateTime)))) invalid("dateTime requires an explicit timezone offset.");
|
|
43
|
-
if (point.timeZone !== undefined) { try { if (typeof point.timeZone !== "string") throw new Error(); new Intl.DateTimeFormat("en", { timeZone: point.timeZone }); } catch { invalid("Use an IANA timeZone."); } }
|
|
44
|
-
}
|
|
74
|
+
for (const point of [body.start, body.end].filter(Boolean)) validateEventTime(point);
|
|
45
75
|
if (body.start && (Boolean(body.start.date) !== Boolean(body.end.date) || Date.parse(body.end.date || body.end.dateTime) <= Date.parse(body.start.date || body.start.dateTime))) invalid("Use matching date types with end after start; all-day end is exclusive.");
|
|
46
|
-
if (body.recurrence !== undefined)
|
|
47
|
-
|
|
48
|
-
if (body.recurrence.length && body.start?.dateTime && (!body.start.timeZone || !body.end.timeZone)) invalid("Timed recurrence requires start/end timeZone.");
|
|
49
|
-
if (body.recurrence.length && !body.start) invalid("Supply start and end when setting recurrence.");
|
|
50
|
-
}
|
|
51
|
-
if (body.attendees !== undefined && (!Array.isArray(body.attendees) || body.attendees.length > 200 || body.attendees.some(person => !person || typeof person.email !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(person.email) || Object.keys(person).some(key => !["email", "displayName", "optional"].includes(key)) || (person.displayName !== undefined && typeof person.displayName !== "string") || (person.optional !== undefined && typeof person.optional !== "boolean")))) invalid("attendees requires up to 200 email entries with optional displayName/optional.");
|
|
76
|
+
if (body.recurrence !== undefined) validateRecurrence(body);
|
|
77
|
+
if (body.attendees !== undefined) validateAttendees(body.attendees);
|
|
52
78
|
if (body.transparency !== undefined && !["opaque", "transparent"].includes(body.transparency)) invalid("Invalid transparency.");
|
|
53
79
|
if (body.visibility !== undefined && !["default", "public", "private", "confidential"].includes(body.visibility)) invalid("Invalid visibility.");
|
|
54
80
|
return body;
|
|
@@ -62,7 +88,7 @@ function writeEvent(method) {
|
|
|
62
88
|
if (!["all", "externalOnly", "none"].includes(sendUpdates)) invalid("Choose sendUpdates explicitly: all, externalOnly or none.");
|
|
63
89
|
if (ifMatch !== undefined && (typeof ifMatch !== "string" || !ifMatch || ifMatch.length > 1024 || /[\r\n]/.test(ifMatch))) invalid("Invalid event ETag.");
|
|
64
90
|
if (method === "DELETE" && event !== undefined) invalid("Cancellation has no event body.");
|
|
65
|
-
const { calendarId, eventId } = validateSchemaPayload({ schema: method === "POST" ?
|
|
91
|
+
const { calendarId, eventId } = validateSchemaPayload({ schema: method === "POST" ? calendarSchema : targetSchema, mode: "replace" }, target, { statusCode: 422 });
|
|
66
92
|
const request = queryUrl(`calendars/${encodeURIComponent(calendarId)}/events${eventId ? `/${encodeURIComponent(eventId)}` : ""}`, { sendUpdates });
|
|
67
93
|
return { ...request, method, ...(ifMatch ? { headers: { "If-Match": ifMatch } } : {}), ...(method !== "DELETE" ? { body: eventBody(event, method === "POST") } : {}) };
|
|
68
94
|
}
|
|
@@ -93,14 +119,14 @@ const googleCalendarProvider = Object.freeze({
|
|
|
93
119
|
}
|
|
94
120
|
},
|
|
95
121
|
"calendars.list": {
|
|
96
|
-
scopes:
|
|
122
|
+
scopes: calendarListScopes,
|
|
97
123
|
validateResult: (result) => result?.kind === "calendar#calendarList" && (result.items === undefined || Array.isArray(result.items)),
|
|
98
124
|
request(input) {
|
|
99
125
|
return queryUrl("users/me/calendarList", validateSchemaPayload({ schema: listSchema, mode: "replace" }, input, { statusCode: 422 }));
|
|
100
126
|
}
|
|
101
127
|
},
|
|
102
128
|
"events.list": {
|
|
103
|
-
scopes:
|
|
129
|
+
scopes: eventListScopes,
|
|
104
130
|
validateResult: (result) => result?.kind === "calendar#events" && (result.items === undefined || Array.isArray(result.items)),
|
|
105
131
|
request(input) {
|
|
106
132
|
const { calendarId, ...query } = validateSchemaPayload({ schema: eventsSchema, mode: "replace" }, input, { statusCode: 422 });
|
package/src/shared/definition.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
const
|
|
1
|
+
const googleScope = (name) => `https://www.googleapis.com/auth/${name}`;
|
|
2
2
|
const googleCalendarDefinition = Object.freeze({
|
|
3
3
|
id: "google-calendar",
|
|
4
4
|
name: "Google Calendar",
|
|
5
5
|
description: "Read calendars and create, edit or cancel events.",
|
|
6
|
-
categories: ["Google", "Productivity"],
|
|
7
6
|
accountModes: ["shared", "per-user", "assistant"],
|
|
8
7
|
authenticationMethods: ["oauth2"],
|
|
9
8
|
setup: {
|
|
@@ -24,29 +23,29 @@ const googleCalendarDefinition = Object.freeze({
|
|
|
24
23
|
]
|
|
25
24
|
},
|
|
26
25
|
scopes: [
|
|
27
|
-
{ value:
|
|
28
|
-
{ value:
|
|
29
|
-
{ value:
|
|
30
|
-
{ value:
|
|
31
|
-
{ value:
|
|
32
|
-
{ value:
|
|
33
|
-
{ value:
|
|
34
|
-
{ value:
|
|
35
|
-
{ value:
|
|
36
|
-
{ value:
|
|
37
|
-
{ value:
|
|
38
|
-
{ value:
|
|
39
|
-
{ value:
|
|
40
|
-
{ value:
|
|
41
|
-
{ value:
|
|
42
|
-
{ value:
|
|
43
|
-
{ value:
|
|
44
|
-
{ value:
|
|
45
|
-
{ value:
|
|
46
|
-
{ value:
|
|
26
|
+
{ value: googleScope("calendar.calendarlist.readonly"), label: "List calendars", recommended: true },
|
|
27
|
+
{ value: googleScope("calendar.events.readonly"), label: "Read events", recommended: true },
|
|
28
|
+
{ value: googleScope("calendar.readonly"), label: "Read all calendar data" },
|
|
29
|
+
{ value: googleScope("calendar"), label: "Manage calendars and events" },
|
|
30
|
+
{ value: googleScope("calendar.calendarlist"), label: "Manage calendar subscriptions" },
|
|
31
|
+
{ value: googleScope("calendar.events"), label: "Manage events" },
|
|
32
|
+
{ value: googleScope("calendar.freebusy"), label: "Read your availability" },
|
|
33
|
+
{ value: googleScope("calendar.settings.readonly"), label: "Read calendar settings" },
|
|
34
|
+
{ value: googleScope("calendar.events.owned"), label: "Manage events in calendars you own" },
|
|
35
|
+
{ value: googleScope("calendar.events.owned.readonly"), label: "Read events in calendars you own" },
|
|
36
|
+
{ value: googleScope("calendar.events.freebusy"), label: "Read availability in accessible calendars" },
|
|
37
|
+
{ value: googleScope("calendar.app.created"), label: "Create and manage app-created calendars" },
|
|
38
|
+
{ value: googleScope("calendar.calendars"), label: "Manage calendar properties" },
|
|
39
|
+
{ value: googleScope("calendar.calendars.readonly"), label: "Read calendar properties" },
|
|
40
|
+
{ value: googleScope("calendar.acls"), label: "Manage calendar sharing permissions" },
|
|
41
|
+
{ value: googleScope("calendar.acls.readonly"), label: "Read calendar sharing permissions" },
|
|
42
|
+
{ value: googleScope("calendar.events.public.readonly"), label: "Read public calendar events" },
|
|
43
|
+
{ value: googleScope("calendar.addons.execute"), label: "Run a Calendar add-on" },
|
|
44
|
+
{ value: googleScope("calendar.addons.current.event.read"), label: "Read the event open in a Calendar add-on" },
|
|
45
|
+
{ value: googleScope("calendar.addons.current.event.write"), label: "Edit the event open in a Calendar add-on" }
|
|
47
46
|
],
|
|
48
47
|
settingsFields: [],
|
|
49
48
|
documentationUrl: "https://developers.google.com/workspace/calendar/api/guides/overview"
|
|
50
49
|
});
|
|
51
50
|
|
|
52
|
-
export { googleCalendarDefinition };
|
|
51
|
+
export { googleCalendarDefinition, googleScope };
|