feat: create calendar events on Google and Microsoft accounts (#22231)
## Context Twenty can import calendar events and send emails, but cannot create calendar events. This adds calendar event creation on connected **Google** and **Microsoft** accounts, mirroring the existing email-send architecture (`message-outbound-manager`). ## What it adds The capability is exposed three ways, all backed by the same composer → driver → persist pipeline: - **GraphQL mutation** `createCalendarEvent` (metadata API) - **AI agent tool** `create_calendar_event` (flows to MCP automatically), gated by a new `CREATE_CALENDAR_EVENT_TOOL` permission flag - **Workflow builder node** "Create Calendar Event" in the **Core** section, with a full settings form (variable interpolation supported) CalDAV/IMAP is intentionally out of scope for now (different long pole). ## Design notes - **Reuse over reinvention** — the created event is run through the existing inbound formatters (`formatGoogleCalendarEvents` / `formatMicrosoftCalendarEvents`) and persisted immediately via the existing `CalendarSaveEventsService`, so it appears in Twenty right away and is reconciled by the next provider sync (dedup on external id). Persistence is best-effort. - **OAuth scopes** — Google already requests `calendar.events` (read+write), so no change there. Microsoft moves `Calendars.Read` → `Calendars.ReadWrite`; existing Microsoft accounts must re-consent (surfaced as a clear "reconnect" error via a missing-scope check). - **Deliberate invitation semantics** — `sendInvitations` is off by default. When off, the event is created with **no attendees** on either provider, so creating an event never silently emails external people. When on, attendees are attached and notified (Google `sendUpdates: all`, Microsoft's default). This sidesteps Microsoft Graph having no per-request suppression. - **Timezone correctness** — Microsoft Graph interprets `dateTime` as wall-clock in the supplied `timeZone` and ignores the offset, so the absolute instant is converted to its wall-clock form before sending (Google honors the offset directly). Both providers end up scheduling the same instant. - **Conferencing** — optional Google Meet (`conferenceData.createRequest`, with a follow-up `events.get` to resolve the async link) / Microsoft Teams (`isOnlineMeeting`). - Attendees are a comma-separated string everywhere (tool input, GraphQL DTO, workflow input), consistent with `send_email` recipients; the composer parses to its internal list. ## Test plan - **Unit**: 45 tests covering the composer (validation, all-day boundaries, offset enforcement, timezone, scope checks, default-account resolution), both provider drivers, the dispatcher, and the workflow step-log builder. - **Integration**: `createCalendarEvent` on the `/metadata` API fails closed with a structured error for a non-existent account (the auth/ownership/validation path that doesn't require provider mocking). - **Manual**: verified the workflow node appears in the Core section, the settings form renders and round-trips (edit → autosave → reload), and the live mutation returns a structured failure for a bogus account. ## Open question for reviewers The metadata mutation `createCalendarEvent` shares a name with the core schema's auto-generated `createCalendarEvent(data:)` CRUD mutation for the CalendarEvent object — they live on different endpoints (`/metadata` vs `/graphql`) so there's no runtime conflict, but it's a potential point of confusion for API consumers. Happy to rename (e.g. `createCalendarEventOnConnectedAccount`) if preferred. ## Out of scope / follow-ups - CalDAV/IMAP support - Event update/delete and recurrence - Existing Microsoft accounts need re-consent for the widened scope <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22231?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: neo773 <neo773@protonmail.com>
This commit is contained in:
@@ -36048,6 +36048,41 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ical-generator@npm:^11.0.0":
|
||||
version: 11.0.0
|
||||
resolution: "ical-generator@npm:11.0.0"
|
||||
peerDependencies:
|
||||
"@touch4it/ical-timezones": ">=1.6.0"
|
||||
"@types/luxon": ">= 1.26.0"
|
||||
"@types/mocha": ">= 8.2.1"
|
||||
dayjs: ">= 1.10.0"
|
||||
luxon: ">= 1.26.0"
|
||||
moment: ">= 2.29.0"
|
||||
moment-timezone: ">= 0.5.33"
|
||||
rrule: ">= 2.6.8"
|
||||
peerDependenciesMeta:
|
||||
"@touch4it/ical-timezones":
|
||||
optional: true
|
||||
"@types/luxon":
|
||||
optional: true
|
||||
"@types/mocha":
|
||||
optional: true
|
||||
"@types/node":
|
||||
optional: true
|
||||
dayjs:
|
||||
optional: true
|
||||
luxon:
|
||||
optional: true
|
||||
moment:
|
||||
optional: true
|
||||
moment-timezone:
|
||||
optional: true
|
||||
rrule:
|
||||
optional: true
|
||||
checksum: 10c0/66bef957e01355ad59ffa060da9086a21f499d5dc79f0a254203a79613c891f880dff33e089af011af6ddb1e4d9af95cba86e7bc5c4001064d3333566469d5d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ico-endec@npm:*":
|
||||
version: 0.1.6
|
||||
resolution: "ico-endec@npm:0.1.6"
|
||||
@@ -53142,6 +53177,7 @@ __metadata:
|
||||
graphql-upload: "npm:16.0.2"
|
||||
graphql-yoga: "npm:4.0.5"
|
||||
html-to-text: "npm:^9.0.5"
|
||||
ical-generator: "npm:^11.0.0"
|
||||
imapflow: "npm:1.4.2"
|
||||
ioredis: "npm:5.10.1"
|
||||
jest: "npm:29.7.0"
|
||||
|
||||
Reference in New Issue
Block a user