-
-
Notifications
You must be signed in to change notification settings - Fork 186
feat: blocked time layer metadata for events #287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||||
| import { detectMeetingLink } from "@analog/meeting-links"; | ||||||||||||||||||
| import { Temporal } from "temporal-polyfill"; | ||||||||||||||||||
| import { z } from "zod/v3"; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { | ||||||||||||||||||
| Attendee, | ||||||||||||||||||
|
|
@@ -10,6 +11,7 @@ import { | |||||||||||||||||
| Recurrence, | ||||||||||||||||||
| } from "../../../interfaces"; | ||||||||||||||||||
| import { CreateEventInput, UpdateEventInput } from "../../../schemas/events"; | ||||||||||||||||||
| import { superjson } from "../../../utils/superjson"; | ||||||||||||||||||
| import { toRecurrenceProperties } from "../../../utils/recurrences/export"; | ||||||||||||||||||
| import { fromRecurrenceProperties } from "../../../utils/recurrences/parse"; | ||||||||||||||||||
| import { | ||||||||||||||||||
|
|
@@ -22,6 +24,11 @@ import { | |||||||||||||||||
| GoogleCalendarEventCreateParams, | ||||||||||||||||||
| } from "./interfaces"; | ||||||||||||||||||
|
|
||||||||||||||||||
| const blockedTimeSchema = z.object({ | ||||||||||||||||||
| before: z.number().positive().optional(), | ||||||||||||||||||
| after: z.number().positive().optional(), | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| export function toGoogleCalendarDate( | ||||||||||||||||||
| value: Temporal.PlainDate | Temporal.Instant | Temporal.ZonedDateTime, | ||||||||||||||||||
| ): GoogleCalendarDate | GoogleCalendarDateTime { | ||||||||||||||||||
|
|
@@ -116,6 +123,34 @@ function parseRecurrence( | |||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| function parseBlockedTime(event: GoogleCalendarEvent) { | ||||||||||||||||||
| const extendedProperties = event.extendedProperties; | ||||||||||||||||||
| if (!extendedProperties?.private && !extendedProperties?.shared) { | ||||||||||||||||||
| return undefined; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const blockedTimeData = | ||||||||||||||||||
| extendedProperties.private?.blockedTime || | ||||||||||||||||||
| extendedProperties.shared?.blockedTime; | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!blockedTimeData) { | ||||||||||||||||||
| return undefined; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| try { | ||||||||||||||||||
| const parsed = superjson.parse(blockedTimeData); | ||||||||||||||||||
| const validated = blockedTimeSchema.parse(parsed); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!validated.before && !validated.after) { | ||||||||||||||||||
| return undefined; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return validated; | ||||||||||||||||||
| } catch { | ||||||||||||||||||
| return undefined; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| interface ParsedGoogleCalendarEventOptions { | ||||||||||||||||||
| calendar: Calendar; | ||||||||||||||||||
| accountId: string; | ||||||||||||||||||
|
|
@@ -135,6 +170,7 @@ export function parseGoogleCalendarEvent({ | |||||||||||||||||
| event, | ||||||||||||||||||
| event.start?.timeZone ?? defaultTimeZone, | ||||||||||||||||||
| ); | ||||||||||||||||||
| const blockedTime = parseBlockedTime(event); | ||||||||||||||||||
|
|
||||||||||||||||||
| return { | ||||||||||||||||||
| // ID should always be present if not defined Google Calendar will generate one | ||||||||||||||||||
|
|
@@ -173,6 +209,11 @@ export function parseGoogleCalendarEvent({ | |||||||||||||||||
| ...(event.recurringEventId && { | ||||||||||||||||||
| recurringEventId: event.recurringEventId, | ||||||||||||||||||
| }), | ||||||||||||||||||
| ...(event.extendedProperties && { | ||||||||||||||||||
| private: event.extendedProperties.private, | ||||||||||||||||||
| shared: event.extendedProperties.shared, | ||||||||||||||||||
| }), | ||||||||||||||||||
| ...(blockedTime && { blockedTime }), | ||||||||||||||||||
| }, | ||||||||||||||||||
| }; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
@@ -205,9 +246,29 @@ function toGoogleCalendarAttendees( | |||||||||||||||||
| return attendees.map(toGoogleCalendarAttendee); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| function toGoogleCalendarBlockedTime(blockedTime: { | ||||||||||||||||||
| before?: number; | ||||||||||||||||||
| after?: number; | ||||||||||||||||||
| }) { | ||||||||||||||||||
| return { | ||||||||||||||||||
| private: { | ||||||||||||||||||
| blockedTime: JSON.stringify(blockedTime), | ||||||||||||||||||
| }, | ||||||||||||||||||
| }; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| export function toGoogleCalendarEvent( | ||||||||||||||||||
| event: CreateEventInput | UpdateEventInput, | ||||||||||||||||||
| ): GoogleCalendarEventCreateParams { | ||||||||||||||||||
| const blockedTimeExtendedProperties = | ||||||||||||||||||
| event.metadata && | ||||||||||||||||||
| "blockedTime" in event.metadata && | ||||||||||||||||||
| event.metadata.blockedTime | ||||||||||||||||||
| ? toGoogleCalendarBlockedTime( | ||||||||||||||||||
| event.metadata.blockedTime as { before?: number; after?: number }, | ||||||||||||||||||
| ) | ||||||||||||||||||
| : undefined; | ||||||||||||||||||
|
|
||||||||||||||||||
| return { | ||||||||||||||||||
| id: event.id, | ||||||||||||||||||
| summary: event.title, | ||||||||||||||||||
|
|
@@ -228,6 +289,9 @@ export function toGoogleCalendarEvent( | |||||||||||||||||
| recurrence: toRecurrenceProperties(event.recurrence), | ||||||||||||||||||
| }), | ||||||||||||||||||
| recurringEventId: event.recurringEventId, | ||||||||||||||||||
| ...(blockedTimeExtendedProperties && { | ||||||||||||||||||
| extendedProperties: blockedTimeExtendedProperties, | ||||||||||||||||||
| }), | ||||||||||||||||||
|
Comment on lines
+292
to
+294
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Emit merged extendedProperties to avoid data loss on updates Use the merged object from above rather than only blockedTime-derived properties. Apply this diff: - ...(blockedTimeExtendedProperties && {
- extendedProperties: blockedTimeExtendedProperties,
- }),
+ ...(mergedExtendedProps && { extendedProperties: mergedExtendedProps }),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| }; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -119,6 +119,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider { | ||||||||||
| .filter( | |||||||||||
| `start/dateTime ge '${startTime}' and end/dateTime le '${endTime}'`, | |||||||||||
| ) | |||||||||||
| .expand("singleValueExtendedProperties") | |||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain$expand for singleValueExtendedProperties should include an $filter and use a stable property ID Microsoft Graph generally requires filtering extended properties by Apply this diff (assumes you export a stable constant - .expand("singleValueExtendedProperties")
+ .expand(
+ `singleValueExtendedProperties($filter=id eq '${MICROSOFT_BLOCKED_TIME_PROP_ID}')`
+ )This ensures the events list returns only the blockedTime property and keeps OData semantics correct. 🌐 Web query: 💡 Result: Yes, Microsoft Graph requires a Official documentation states:
Example from the official docs: GET https://graph.microsoft.com/v1.0/me/events?$expand=singleValueExtendedProperties($filter=id eq 'String {GUID} Name CustomProperty')This request will expand the event with only the extended property matching the given Community threads and support answers clarify:
Representative code snippet from a support answer: .Expand("singleValueExtendedProperties($filter=id eq 'String {GUID} Name MyExtendedProperty')")Retrieves only the extended property with the specified In summary: Reference - official documentation snippet:
If you need to retrieve all singleValueExtendedProperties for an event, the Graph API currently does not support a wildcard or omitting the filter; you must query those properties individually by their ID[3][5][8]. Citations:
Include Microsoft Graph requires specifying a Key location:
Suggested diff (assuming you export - .expand("singleValueExtendedProperties")
+ .expand(
+ `singleValueExtendedProperties($filter=id eq '${MICROSOFT_BLOCKED_TIME_PROP_ID}')`
+ )This ensures the events list returns only the blockedTime property and conforms to OData requirements. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
|||||||||||
| .orderby("start/dateTime") | |||||||||||
| .top(CALENDAR_DEFAULTS.MAX_EVENTS_PER_CALENDAR) | |||||||||||
| .get(); | |||||||||||
|
|
@@ -163,6 +164,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider { | ||||||||||
| return this.withErrorHandler("createEvent", async () => { | |||||||||||
| const createdEvent: MicrosoftEvent = await this.graphClient | |||||||||||
| .api(`${calendarPath(calendar.id)}/events`) | |||||||||||
| .expand("singleValueExtendedProperties") | |||||||||||
| .post(toMicrosoftEvent(event)); | |||||||||||
|
Comment on lines
+167
to
168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainRemove $expand from POST create; prefer return=representation if you need the created entity OData options like Apply this diff: - .api(`${calendarPath(calendar.id)}/events`)
- .expand("singleValueExtendedProperties")
- .post(toMicrosoftEvent(event));
+ .api(`${calendarPath(calendar.id)}/events`)
+ .header("Prefer", 'return=representation')
+ .post(toMicrosoftEvent(event));Optional: If you must guarantee the extended property in the response, perform a subsequent GET with the same 🌐 Web query: 💡 Result: Microsoft Graph does not support using Details
Workflow Recommendation
Additional Considerations
Summary Table:
This approach is explicitly confirmed in Microsoft Graph documentation regarding extended properties and Citations:
Remove unsupported The To receive the created event entity in the response, rely on the default representation returned by POST (or explicitly request it) using the Extended properties included in the request body are persisted but not returned in the POST response. To retrieve them immediately, issue a follow-up GET on the event with Apply this diff in - .api(`${calendarPath(calendar.id)}/events`)
- .expand("singleValueExtendedProperties")
- .post(toMicrosoftEvent(event));
+ .api(`${calendarPath(calendar.id)}/events`)
+ .header("Prefer", "return=representation")
+ .post(toMicrosoftEvent(event));Optional: to guarantee extended properties in the response, perform: const created = await client
.api(`${calendarPath(calendar.id)}/events/${createdEvent.id}`)
.expand("singleValueExtendedProperties($filter=id eq '<your-prop-id>')")
.get();🤖 Prompt for AI Agents |
|||||||||||
|
|
|||||||||||
| return parseMicrosoftEvent({ | |||||||||||
|
|
@@ -190,6 +192,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider { | ||||||||||
| // First, perform the regular event update | |||||||||||
| const updatedEvent: MicrosoftEvent = await this.graphClient | |||||||||||
| .api(`${calendarPath(calendar.id)}/events/${eventId}`) | |||||||||||
| .expand("singleValueExtendedProperties") | |||||||||||
| .patch(toMicrosoftEvent(event)); | |||||||||||
|
Comment on lines
+195
to
196
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainRemove $expand from PATCH update; ensure you get a representation Similar to create, Apply this diff: - .api(`${calendarPath(calendar.id)}/events/${eventId}`)
- .expand("singleValueExtendedProperties")
- .patch(toMicrosoftEvent(event));
+ .api(`${calendarPath(calendar.id)}/events/${eventId}`)
+ .header("Prefer", 'return=representation')
+ .patch(toMicrosoftEvent(event));Follow-up: If the API still returns 204 No Content depending on resource, perform a GET to fetch the updated record with 🌐 Web query: 💡 Result: When updating an event via Microsoft Graph using PATCH, the response does not return a resource representation by default; instead, it returns a 204 No Content by default unless you include the Supporting details:
In summary: PATCHing an event in Microsoft Graph returns 204 No Content by default; use Citations:
Ensure PATCH returns the updated event by replacing The Graph API’s PATCH method does not return a resource by default (it responds with HTTP 204 No Content unless you include a • Location: Apply this fix: - .api(`${calendarPath(calendar.id)}/events/${eventId}`)
- .expand("singleValueExtendedProperties")
- .patch(toMicrosoftEvent(event));
+ .api(`${calendarPath(calendar.id)}/events/${eventId}`)
+ .header("Prefer", "return=representation")
+ .patch(toMicrosoftEvent(event));Follow-up: If you still receive 204 No Content (which can occur for some resources even with the header), immediately issue a GET on the same endpoint using: .api(`${calendarPath(calendar.id)}/events/${eventId}`)
.expand("singleValueExtendedProperties")
.get();to retrieve the fully populated event. 🤖 Prompt for AI Agents |
|||||||||||
|
|
|||||||||||
| // Then, handle response status update if present (Microsoft-specific approach) | |||||||||||
|
|
|||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -100,6 +100,36 @@ function parseResponseStatus( | |||||||||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| function parseBlockedTime(event: MicrosoftEvent) { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!event.singleValueExtendedProperties) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const blockedTimeProperty = event.singleValueExtendedProperties.find( | ||||||||||||||||||||||||||||||||||||||||||||||
| (prop) => prop && prop.id && prop.id.includes("Name blockedTime"), | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (!blockedTimeProperty?.value) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = JSON.parse(blockedTimeProperty.value); | ||||||||||||||||||||||||||||||||||||||||||||||
| const result: { before?: number; after?: number } = {}; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof parsed.before === "number" && parsed.before > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||
| result.before = parsed.before; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof parsed.after === "number" && parsed.after > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||
| result.after = parsed.after; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return Object.keys(result).length > 0 ? result : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+103
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Parsing extended properties should target a stable property ID, not a name substring Filtering by Apply this diff (and export the constant for reuse by the provider’s GET): +// Use a stable property set GUID that is constant for your app,
+// not a per-event random UUID. Generate once and commit it.
+export const MICROSOFT_BLOCKED_TIME_PROP_ID =
+ "String {f8a0fe66-219a-4e34-93da-e5edbe9801bc} Name blockedTime";
+
function parseBlockedTime(event: MicrosoftEvent) {
if (!event.singleValueExtendedProperties) {
return undefined;
}
- const blockedTimeProperty = event.singleValueExtendedProperties.find(
- (prop) => prop && prop.id && prop.id.includes("Name blockedTime"),
- );
+ const blockedTimeProperty = event.singleValueExtendedProperties.find(
+ (prop) => prop?.id === MICROSOFT_BLOCKED_TIME_PROP_ID,
+ );This aligns parsing with the |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export function parseMicrosoftEvent({ | ||||||||||||||||||||||||||||||||||||||||||||||
| accountId, | ||||||||||||||||||||||||||||||||||||||||||||||
| calendar, | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -112,6 +142,7 @@ export function parseMicrosoftEvent({ | |||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const responseStatus = parseResponseStatus(event); | ||||||||||||||||||||||||||||||||||||||||||||||
| const blockedTime = parseBlockedTime(event); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||
| id: event.id!, | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -157,6 +188,7 @@ export function parseMicrosoftEvent({ | |||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| : {}), | ||||||||||||||||||||||||||||||||||||||||||||||
| onlineMeeting: event.onlineMeeting, | ||||||||||||||||||||||||||||||||||||||||||||||
| ...(blockedTime && { blockedTime }), | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -199,10 +231,30 @@ function toMicrosoftConferenceData(conference: Conference) { | |||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| function toMicrosoftBlockedTime(blockedTime: { | ||||||||||||||||||||||||||||||||||||||||||||||
| before?: number; | ||||||||||||||||||||||||||||||||||||||||||||||
| after?: number; | ||||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return [ | ||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||
| id: `String {${crypto.randomUUID()}} Name blockedTime`, | ||||||||||||||||||||||||||||||||||||||||||||||
| value: JSON.stringify(blockedTime), | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+234
to
+244
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t generate a new GUID per write; use a constant property set ID and import crypto correctly if needed
Apply this diff to use the same stable ID and drop the random UUID: -function toMicrosoftBlockedTime(blockedTime: {
+function toMicrosoftBlockedTime(blockedTime: {
before?: number;
after?: number;
}) {
return [
{
- id: `String {${crypto.randomUUID()}} Name blockedTime`,
+ id: MICROSOFT_BLOCKED_TIME_PROP_ID,
value: JSON.stringify(blockedTime),
},
];
}If you still need 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export function toMicrosoftEvent( | ||||||||||||||||||||||||||||||||||||||||||||||
| event: CreateEventInput | UpdateEventInput, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): MicrosoftEvent { | ||||||||||||||||||||||||||||||||||||||||||||||
| const metadata = event.metadata as MicrosoftEventMetadata | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||
| const blockedTimeProperties = | ||||||||||||||||||||||||||||||||||||||||||||||
| event.metadata && | ||||||||||||||||||||||||||||||||||||||||||||||
| "blockedTime" in event.metadata && | ||||||||||||||||||||||||||||||||||||||||||||||
| event.metadata.blockedTime | ||||||||||||||||||||||||||||||||||||||||||||||
| ? toMicrosoftBlockedTime( | ||||||||||||||||||||||||||||||||||||||||||||||
| event.metadata.blockedTime as { before?: number; after?: number }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||
| subject: event.title, | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -220,6 +272,9 @@ export function toMicrosoftEvent( | |||||||||||||||||||||||||||||||||||||||||||||
| isAllDay: event.allDay ?? false, | ||||||||||||||||||||||||||||||||||||||||||||||
| location: event.location ? { displayName: event.location } : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||
| // ...(event.conference && toMicrosoftConferenceData(event.conference)), | ||||||||||||||||||||||||||||||||||||||||||||||
| ...(blockedTimeProperties && { | ||||||||||||||||||||||||||||||||||||||||||||||
| singleValueExtendedProperties: blockedTimeProperties, | ||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -67,6 +67,12 @@ const microsoftMetadataSchema = z.object({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| tollNumber: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| blockedTime: z | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| before: z.number().int().positive().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| after: z.number().int().positive().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Deduplicate and harden blockedTime schema You define the same Apply this diff (add the shared schema once, then reuse): import { z } from "zod/v3";
+const blockedTimeSchema = z
+ .object({
+ before: z.number().int().positive().optional(),
+ after: z.number().int().positive().optional(),
+ })
+ .refine((v) => typeof v.before === "number" || typeof v.after === "number", {
+ message: "blockedTime must include at least one of 'before' or 'after'",
+ path: [],
+ });
+
const microsoftMetadataSchema = z.object({
@@
- blockedTime: z
- .object({
- before: z.number().int().positive().optional(),
- after: z.number().int().positive().optional(),
- })
- .optional(),
+ blockedTime: blockedTimeSchema.optional(),📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const googleMetadataSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -101,6 +107,15 @@ const googleMetadataSchema = z.object({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| originalRecurrence: z.array(z.string()).optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Store the recurring event ID for instances of recurring events | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recurringEventId: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extended properties for custom data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private: z.record(z.string(), z.string()).optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shared: z.record(z.string(), z.string()).optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| blockedTime: z | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| before: z.number().int().positive().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| after: z.number().int().positive().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+110
to
119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Reuse the shared blockedTime schema for Google metadata as well Keep the constraints identical across providers. Apply this diff: - blockedTime: z
- .object({
- before: z.number().int().positive().optional(),
- after: z.number().int().positive().optional(),
- })
- .optional(),
+ blockedTime: blockedTimeSchema.optional(),📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const dateInputSchema = z.union([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Compute extendedProperties once and merge with any user-provided maps
Right now you only include
extendedPropertieswhenblockedTimeis present, and you don’t merge other keys frommetadata.private/metadata.shared. On updates, this risks overwriting/losing existing keys.Apply this diff to merge both sources:
Then below, emit
extendedProperties: mergedExtendedPropswhen defined.🤖 Prompt for AI Agents