-
-
Notifications
You must be signed in to change notification settings - Fork 186
feat(api): add google calendar sync and change channel support #348
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?
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
22 issues found across 22 files
React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.
packages/auth/src/storage.ts
Outdated
| @@ -0,0 +1,27 @@ | |||
| import { Redis } from "@upstash/redis"; | |||
| import { SecondaryStorage } from "better-auth"; | |||
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.
Use a type-only import for SecondaryStorage to avoid adding an unnecessary runtime dependency to the bundle.
(This reflects your team's feedback about using type-only imports for types to keep bundles lean.)
Prompt for AI agents
Address the following comment on packages/auth/src/storage.ts at line 2:
<comment>Use a type-only import for SecondaryStorage to avoid adding an unnecessary runtime dependency to the bundle.
(This reflects your team's feedback about using type-only imports for types to keep bundles lean.)</comment>
<file context>
@@ -0,0 +1,27 @@
+import { Redis } from "@upstash/redis";
+import { SecondaryStorage } from "better-auth";
+
+import { env } from "@repo/env/server";
</file context>
| import { SecondaryStorage } from "better-auth"; | |
| import type { SecondaryStorage } from "better-auth"; |
packages/auth/src/storage.ts
Outdated
| get: async (key) => { | ||
| const value = await redis.get<string>(key); | ||
|
|
||
| return value ?? null; |
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.
Ensure get always returns a string or null; stringify non-string values to prevent downstream type/parse errors.
(Based on your team's feedback about strict adherence to SecondaryStorage string contract to avoid runtime inconsistencies.)
Prompt for AI agents
Address the following comment on packages/auth/src/storage.ts at line 15:
<comment>Ensure get always returns a string or null; stringify non-string values to prevent downstream type/parse errors.
(Based on your team's feedback about strict adherence to SecondaryStorage string contract to avoid runtime inconsistencies.)</comment>
<file context>
@@ -0,0 +1,27 @@
+ get: async (key) => {
+ const value = await redis.get<string>(key);
+
+ return value ?? null;
+ },
+ set: async (key, value, ttl) => {
</file context>
| return new Response("Channel not found", { status: 404 }); | ||
| } | ||
|
|
||
| if (headers.token && headers.token !== channel.token) { |
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.
Channel token not enforced when missing, allowing spoofed notifications without token
Prompt for AI agents
Address the following comment on packages/api/src/providers/google-calendar/channel.ts at line 67:
<comment>Channel token not enforced when missing, allowing spoofed notifications without token</comment>
<file context>
@@ -0,0 +1,115 @@
+ return new Response("Channel not found", { status: 404 });
+ }
+
+ if (headers.token && headers.token !== channel.token) {
+ return new Response("Invalid channel token", { status: 401 });
+ }
</file context>
| refreshTokenExpiresAt: timestamp(), | ||
| scope: text(), | ||
| password: text(), | ||
| calendarListSyncToken: text("calendar_list_sync_token"), |
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.
Schema change without migration: added account.calendarListSyncToken but no DB migration adds the column, causing runtime query errors
Prompt for AI agents
Address the following comment on packages/db/src/schema/auth.ts at line 80:
<comment>Schema change without migration: added account.calendarListSyncToken but no DB migration adds the column, causing runtime query errors</comment>
<file context>
@@ -77,6 +77,7 @@ export const account = pgTable(
refreshTokenExpiresAt: timestamp(),
scope: text(),
password: text(),
+ calendarListSyncToken: text("calendar_list_sync_token"),
createdAt: timestamp().notNull(),
updatedAt: timestamp().notNull(),
</file context>
| type: "google.calendar" as const, | ||
| subscriptionId, | ||
| resourceId: response.resourceId!, | ||
| expiresAt: new Date(response.expiration!), |
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.
expiration is a string millisecond timestamp; constructing Date with a numeric string yields Invalid Date, causing incorrect expiresAt and potential runtime errors.
Prompt for AI agents
Address the following comment on packages/api/src/providers/google-calendar/subscribe.ts at line 29:
<comment>expiration is a string millisecond timestamp; constructing Date with a numeric string yields Invalid Date, causing incorrect expiresAt and potential runtime errors.</comment>
<file context>
@@ -0,0 +1,79 @@
+ type: "google.calendar" as const,
+ subscriptionId,
+ resourceId: response.resourceId!,
+ expiresAt: new Date(response.expiration!),
+ };
+}
</file context>
packages/db/src/schema/channel.ts
Outdated
| .$default(() => newId("channel")), | ||
|
|
||
| // TODO: when an account is deleted, we should first stop channel subscriptions and then delete all channels associated with it | ||
| accountId: text() |
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.
Add indexes for accountId and resourceId to avoid slow lookups and joins; consider a composite index if queries filter by both.
Prompt for AI agents
Address the following comment on packages/db/src/schema/channel.ts at line 13:
<comment>Add indexes for accountId and resourceId to avoid slow lookups and joins; consider a composite index if queries filter by both.</comment>
<file context>
@@ -0,0 +1,31 @@
+ .$default(() => newId("channel")),
+
+ // TODO: when an account is deleted, we should first stop channel subscriptions and then delete all channels associated with it
+ accountId: text()
+ .notNull()
+ .references(() => account.id, { onDelete: "cascade" }),
</file context>
| @@ -0,0 +1,40 @@ | |||
| import { z } from "zod"; | |||
|
|
|||
| import { channel } from "@repo/db/schema"; | |||
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.
Import is used only in a type position; prefer a type-only import to avoid an unnecessary runtime dependency and improve tree-shaking.
Prompt for AI agents
Address the following comment on packages/api/src/providers/google-calendar/channels/headers.ts at line 3:
<comment>Import is used only in a type position; prefer a type-only import to avoid an unnecessary runtime dependency and improve tree-shaking.</comment>
<file context>
@@ -0,0 +1,40 @@
+import { z } from "zod";
+
+import { channel } from "@repo/db/schema";
+
+export type ChannelHeaders = z.infer<typeof headersSchema>;
</file context>
| return parseGoogleCalendarEvent({ | ||
| calendar: destinationCalendar, | ||
| calendarId: destinationCalendar.id, | ||
| readOnly: false, |
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.
Hard-coding readOnly to false misrepresents destination calendar permissions; use destinationCalendar.readOnly for correctness and consistency.
Prompt for AI agents
Address the following comment on packages/api/src/providers/calendars/google-calendar.ts at line 281:
<comment>Hard-coding readOnly to false misrepresents destination calendar permissions; use destinationCalendar.readOnly for correctness and consistency.</comment>
<file context>
@@ -273,7 +277,8 @@ export class GoogleCalendarProvider implements CalendarProvider {
return parseGoogleCalendarEvent({
- calendar: destinationCalendar,
+ calendarId: destinationCalendar.id,
+ readOnly: false,
accountId: this.accountId,
event: moved,
</file context>
| readOnly: false, | |
| readOnly: destinationCalendar.readOnly, |
| calendarId: calendar.id, | ||
| }) | ||
| .onConflictDoUpdate({ | ||
| target: [calendars.id], |
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.
Primary key conflates calendars across accounts; upsert updates cross-tenant due to conflict on id only.
Prompt for AI agents
Address the following comment on packages/api/src/providers/google-calendar/channels/calendars.ts at line 21:
<comment>Primary key conflates calendars across accounts; upsert updates cross-tenant due to conflict on id only.</comment>
<file context>
@@ -0,0 +1,74 @@
+ calendarId: calendar.id,
+ })
+ .onConflictDoUpdate({
+ target: [calendars.id],
+ set: calendar,
+ });
</file context>
packages/db/src/schema/resource.ts
Outdated
| export const resource = pgTable("resource", { | ||
| id: text() | ||
| .primaryKey() | ||
| .$default(() => newId("resource")), |
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.
Use $defaultFn for function-based default so newId("resource") is evaluated per insert.
Prompt for AI agents
Address the following comment on packages/db/src/schema/resource.ts at line 8:
<comment>Use $defaultFn for function-based default so newId("resource") is evaluated per insert.</comment>
<file context>
@@ -0,0 +1,16 @@
+export const resource = pgTable("resource", {
+ id: text()
+ .primaryKey()
+ .$default(() => newId("resource")),
+ providerId: text({ enum: ["google"] }).notNull(),
+
</file context>
d6f84f7 to
489e9f2
Compare
Description
Briefly describe what you did and why.
Screenshots / Recordings
Add screenshots or recordings here to help reviewers understand your changes.
Type of Change
Related Areas
Testing
Checklist
Notes
(Optional) Add anything else you'd like to share.
By submitting, I confirm I understand and stand behind this code. If AI was used, I’ve reviewed and verified everything myself.
Summary by cubic
Adds Google Calendar change channels (push notifications) and incremental sync for calendar lists and events via new webhook endpoints. Expands the data model to store channels/resources and richer event data, and updates providers for reliable token-based sync.
New Features
Migration