From 7b7b34ed5c5d795fd659a687ebe0b0e9e16d5986 Mon Sep 17 00:00:00 2001 From: DennieDan Date: Fri, 20 Jun 2025 13:57:47 +0800 Subject: [PATCH 1/5] feat: add migration of workspace_calendar_sync_log relation --- ...134409_add_workspace_calendar_sync_log.sql | 91 +++++++++ packages/types/src/supabase.ts | 188 +++++++++++------- 2 files changed, 210 insertions(+), 69 deletions(-) create mode 100644 apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql diff --git a/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql b/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql new file mode 100644 index 000000000..85f9b98e1 --- /dev/null +++ b/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql @@ -0,0 +1,91 @@ +-- Add workspace_calendar_sync_log table to track calendar sync operations + +-- Create workspace_calendar_sync_log table +CREATE TABLE "public"."workspace_calendar_sync_log" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "ws_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, + "google_account_id" text NOT NULL, + "sync_started_at" timestamp with time zone NOT NULL, + "sync_ended_at" timestamp with time zone, + "status" text NOT NULL, + "error_message" text, + "event_snapshot_before" jsonb NOT NULL, + "event_snapshot_after" jsonb, + "triggered_by" text NOT NULL, + "created_at" timestamp with time zone NOT NULL DEFAULT now() +); + +-- Enable Row Level Security +ALTER TABLE "public"."workspace_calendar_sync_log" ENABLE ROW LEVEL SECURITY; + +-- Create primary key +CREATE UNIQUE INDEX workspace_calendar_sync_log_pkey ON public.workspace_calendar_sync_log USING btree (id); +ALTER TABLE "public"."workspace_calendar_sync_log" ADD CONSTRAINT "workspace_calendar_sync_log_pkey" PRIMARY KEY using index "workspace_calendar_sync_log_pkey"; + +-- Create indexes for better query performance +CREATE INDEX workspace_calendar_sync_log_workspace_id_idx ON public.workspace_calendar_sync_log USING btree (ws_id); +CREATE INDEX workspace_calendar_sync_log_google_account_id_idx ON public.workspace_calendar_sync_log USING btree (google_account_id); +CREATE INDEX workspace_calendar_sync_log_status_idx ON public.workspace_calendar_sync_log USING btree (status); +CREATE INDEX workspace_calendar_sync_log_sync_started_at_idx ON public.workspace_calendar_sync_log USING btree (sync_started_at); + +-- RLS Policies +-- Users can see sync logs for workspaces they are members of +CREATE POLICY "Users can view sync logs for their workspaces" +ON public.workspace_calendar_sync_log +FOR SELECT +TO authenticated +USING ( + EXISTS ( + SELECT 1 FROM public.workspace_members wm + WHERE wm.ws_id = ws_id + AND wm.user_id = auth.uid() + ) +); + +-- Users can insert sync logs for workspaces they are members of +CREATE POLICY "Users can insert sync logs for their workspaces" +ON public.workspace_calendar_sync_log +FOR INSERT +TO authenticated +WITH CHECK ( + EXISTS ( + SELECT 1 FROM public.workspace_members wm + WHERE wm.ws_id = ws_id + AND wm.user_id = auth.uid() + ) +); + +-- Users can update sync logs for workspaces they are members of +CREATE POLICY "Users can update sync logs for their workspaces" +ON public.workspace_calendar_sync_log +FOR UPDATE +TO authenticated +USING ( + EXISTS ( + SELECT 1 FROM public.workspace_members wm + WHERE wm.ws_id = ws_id + AND wm.user_id = auth.uid() + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 FROM public.workspace_members wm + WHERE wm.ws_id = ws_id + AND wm.user_id = auth.uid() + ) +); + +-- Add check constraint for status values +ALTER TABLE "public"."workspace_calendar_sync_log" +ADD CONSTRAINT "workspace_calendar_sync_log_status_check" +CHECK (status IN ('success', 'failed', 'in_progress', 'cancelled', 'partial_success')); + +-- Add check constraint for triggered_by values +ALTER TABLE "public"."workspace_calendar_sync_log" +ADD CONSTRAINT "workspace_calendar_sync_log_triggered_by_check" +CHECK (triggered_by IN ('active_sync', 'trigger_dev', 'manual')); + +-- Add check constraint to ensure sync_ended_at is after sync_started_at when both are present +ALTER TABLE "public"."workspace_calendar_sync_log" +ADD CONSTRAINT "workspace_calendar_sync_log_timestamps_check" +CHECK (sync_ended_at IS NULL OR sync_ended_at >= sync_started_at); \ No newline at end of file diff --git a/packages/types/src/supabase.ts b/packages/types/src/supabase.ts index ec2dbc191..d5921fe18 100644 --- a/packages/types/src/supabase.ts +++ b/packages/types/src/supabase.ts @@ -5062,6 +5062,56 @@ export type Database = { }, ]; }; + workspace_calendar_sync_log: { + Row: { + created_at: string; + error_message: string | null; + event_snapshot_after: Json | null; + event_snapshot_before: Json; + google_account_id: string; + id: string; + status: string; + sync_ended_at: string | null; + sync_started_at: string; + triggered_by: string; + ws_id: string; + }; + Insert: { + created_at?: string; + error_message?: string | null; + event_snapshot_after?: Json | null; + event_snapshot_before: Json; + google_account_id: string; + id?: string; + status: string; + sync_ended_at?: string | null; + sync_started_at: string; + triggered_by: string; + ws_id: string; + }; + Update: { + created_at?: string; + error_message?: string | null; + event_snapshot_after?: Json | null; + event_snapshot_before?: Json; + google_account_id?: string; + id?: string; + status?: string; + sync_ended_at?: string | null; + sync_started_at?: string; + triggered_by?: string; + ws_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'workspace_calendar_sync_log_ws_id_fkey'; + columns: ['ws_id']; + isOneToOne: false; + referencedRelation: 'workspaces'; + referencedColumns: ['id']; + }, + ]; + }; workspace_configs: { Row: { created_at: string; @@ -7631,56 +7681,56 @@ export type Database = { }; count_search_users: { Args: + | { search_query: string } | { - enabled_filter?: boolean; search_query: string; role_filter?: string; - } - | { search_query: string }; + enabled_filter?: boolean; + }; Returns: number; }; create_ai_chat: { - Args: { message: string; title: string; model: string }; + Args: { title: string; message: string; model: string }; Returns: string; }; generate_cross_app_token: { Args: | { + p_user_id: string; p_origin_app: string; - p_expiry_seconds?: number; p_target_app: string; - p_user_id: string; + p_expiry_seconds?: number; } | { + p_user_id: string; p_origin_app: string; - p_session_data?: Json; - p_expiry_seconds?: number; p_target_app: string; - p_user_id: string; + p_expiry_seconds?: number; + p_session_data?: Json; }; Returns: string; }; get_challenge_stats: { - Args: { user_id_param: string; challenge_id_param: string }; + Args: { challenge_id_param: string; user_id_param: string }; Returns: { - problems_attempted: number; total_score: number; + problems_attempted: number; }[]; }; get_daily_income_expense: { - Args: { past_days?: number; _ws_id: string }; + Args: { _ws_id: string; past_days?: number }; Returns: { day: string; - total_expense: number; total_income: number; + total_expense: number; }[]; }; get_daily_prompt_completion_tokens: { Args: { past_days?: number }; Returns: { + day: string; total_prompt_tokens: number; total_completion_tokens: number; - day: string; }[]; }; get_finance_invoices_count: { @@ -7706,9 +7756,9 @@ export type Database = { get_hourly_prompt_completion_tokens: { Args: { past_hours?: number }; Returns: { + hour: string; total_prompt_tokens: number; total_completion_tokens: number; - hour: string; }[]; }; get_inventory_batches_count: { @@ -7727,9 +7777,9 @@ export type Database = { _has_unit?: boolean; }; Returns: { - manufacturer: string; id: string; name: string; + manufacturer: string; unit: string; unit_id: string; category: string; @@ -7758,8 +7808,8 @@ export type Database = { get_monthly_income_expense: { Args: { _ws_id: string; past_months?: number }; Returns: { - total_income: number; month: string; + total_income: number; total_expense: number; }[]; }; @@ -7776,31 +7826,31 @@ export type Database = { Returns: number; }; get_possible_excluded_groups: { - Args: { included_groups: string[]; _ws_id: string }; + Args: { _ws_id: string; included_groups: string[] }; Returns: { - amount: number; id: string; name: string; ws_id: string; + amount: number; }[]; }; get_possible_excluded_tags: { Args: { _ws_id: string; included_tags: string[] }; Returns: { - ws_id: string; id: string; name: string; + ws_id: string; amount: number; }[]; }; get_session_statistics: { Args: Record; Returns: { - latest_session_date: string; - active_count: number; + total_count: number; unique_users_count: number; + active_count: number; completed_count: number; - total_count: number; + latest_session_date: string; }[]; }; get_session_templates: { @@ -7810,8 +7860,8 @@ export type Database = { limit_count?: number; }; Returns: { - description: string; title: string; + description: string; category_id: string; task_id: string; tags: string[]; @@ -7826,20 +7876,20 @@ export type Database = { get_submission_statistics: { Args: Record; Returns: { + total_count: number; latest_submission_date: string; unique_users_count: number; - total_count: number; }[]; }; get_transaction_categories_with_amount: { Args: Record; Returns: { - created_at: string; - is_expense: boolean; - name: string; id: string; - amount: number; + name: string; + is_expense: boolean; ws_id: string; + created_at: string; + amount: number; }[]; }; get_user_role: { @@ -7857,36 +7907,36 @@ export type Database = { get_user_sessions: { Args: { user_id: string }; Returns: { + session_id: string; + created_at: string; + updated_at: string; user_agent: string; ip: string; is_current: boolean; - created_at: string; - updated_at: string; - session_id: string; }[]; }; get_user_tasks: { Args: { _board_id: string }; Returns: { - priority: number; - board_id: string; - list_id: string; - end_date: string; - start_date: string; id: string; name: string; description: string; + priority: number; completed: boolean; + start_date: string; + end_date: string; + list_id: string; + board_id: string; }[]; }; get_user_whitelist_status: { Args: { user_id_param: string }; Returns: { is_whitelisted: boolean; - allow_role_management: boolean; + enabled: boolean; allow_challenge_management: boolean; allow_manage_all_challenges: boolean; - enabled: boolean; + allow_role_management: boolean; }[]; }; get_workspace_drive_size: { @@ -7902,7 +7952,7 @@ export type Database = { Returns: number; }; get_workspace_transactions_count: { - Args: { start_date?: string; ws_id: string; end_date?: string }; + Args: { ws_id: string; start_date?: string; end_date?: string }; Returns: number; }; get_workspace_user_groups: { @@ -7913,13 +7963,13 @@ export type Database = { search_query: string; }; Returns: { - ws_id: string; - tags: string[]; - created_at: string; - tag_count: number; id: string; name: string; notes: string; + ws_id: string; + tags: string[]; + tag_count: number; + created_at: string; }[]; }; get_workspace_user_groups_count: { @@ -7928,12 +7978,18 @@ export type Database = { }; get_workspace_users: { Args: { - search_query: string; - excluded_groups: string[]; - included_groups: string[]; _ws_id: string; + included_groups: string[]; + excluded_groups: string[]; + search_query: string; }; Returns: { + id: string; + avatar_url: string; + full_name: string; + display_name: string; + email: string; + phone: string; gender: string; birthday: string; ethnicity: string; @@ -7948,12 +8004,6 @@ export type Database = { linked_users: Json; created_at: string; updated_at: string; - email: string; - full_name: string; - avatar_url: string; - id: string; - phone: string; - display_name: string; }[]; }; get_workspace_users_count: { @@ -7965,19 +8015,19 @@ export type Database = { Returns: number; }; get_workspace_wallets_expense: { - Args: { end_date?: string; start_date?: string; ws_id: string }; + Args: { ws_id: string; start_date?: string; end_date?: string }; Returns: number; }; get_workspace_wallets_income: { - Args: { end_date?: string; start_date?: string; ws_id: string }; + Args: { ws_id: string; start_date?: string; end_date?: string }; Returns: number; }; has_other_owner: { - Args: { _user_id: string; _ws_id: string }; + Args: { _ws_id: string; _user_id: string }; Returns: boolean; }; insert_ai_chat_message: { - Args: { source: string; chat_id: string; message: string }; + Args: { message: string; chat_id: string; source: string }; Returns: undefined; }; is_list_accessible: { @@ -7985,7 +8035,7 @@ export type Database = { Returns: boolean; }; is_member_invited: { - Args: { _org_id: string; _user_id: string }; + Args: { _user_id: string; _org_id: string }; Returns: boolean; }; is_nova_challenge_manager: { @@ -8001,11 +8051,11 @@ export type Database = { Returns: boolean; }; is_nova_user_id_in_team: { - Args: { _team_id: string; _user_id: string }; + Args: { _user_id: string; _team_id: string }; Returns: boolean; }; is_org_member: { - Args: { _org_id: string; _user_id: string }; + Args: { _user_id: string; _org_id: string }; Returns: boolean; }; is_project_member: { @@ -8037,7 +8087,7 @@ export type Database = { Returns: Json; }; nova_get_user_daily_sessions: { - Args: { user_id: string; challenge_id: string }; + Args: { challenge_id: string; user_id: string }; Returns: number; }; nova_get_user_total_sessions: { @@ -8067,7 +8117,6 @@ export type Database = { enabled_filter?: boolean; }; Returns: { - team_name: string[]; id: string; display_name: string; deleted: boolean; @@ -8083,6 +8132,7 @@ export type Database = { email: string; new_email: string; birthday: string; + team_name: string[]; }[]; }; search_users_by_name: { @@ -8092,11 +8142,11 @@ export type Database = { min_similarity?: number; }; Returns: { - display_name: string; - relevance: number; - avatar_url: string; id: string; handle: string; + display_name: string; + avatar_url: string; + relevance: number; }[]; }; sum_quiz_scores: { @@ -8106,11 +8156,11 @@ export type Database = { }[]; }; transactions_have_same_abs_amount: { - Args: { transaction_id_2: string; transaction_id_1: string }; + Args: { transaction_id_1: string; transaction_id_2: string }; Returns: boolean; }; transactions_have_same_amount: { - Args: { transaction_id_2: string; transaction_id_1: string }; + Args: { transaction_id_1: string; transaction_id_2: string }; Returns: boolean; }; update_expired_sessions: { @@ -8122,11 +8172,11 @@ export type Database = { Returns: undefined; }; validate_cross_app_token: { - Args: { p_target_app: string; p_token: string }; + Args: { p_token: string; p_target_app: string }; Returns: string; }; validate_cross_app_token_with_session: { - Args: { p_target_app: string; p_token: string }; + Args: { p_token: string; p_target_app: string }; Returns: { user_id: string; session_data: Json; From 9cea4a209cf5042bd8b13f3b05f0aaef84f46020 Mon Sep 17 00:00:00 2001 From: DennieDan Date: Fri, 20 Jun 2025 15:15:53 +0800 Subject: [PATCH 2/5] feat: add logging for background --- ...134409_add_workspace_calendar_sync_log.sql | 9 +- packages/trigger/google-calendar-sync.ts | 128 +++++++++++++++++- packages/types/src/supabase.ts | 18 ++- 3 files changed, 144 insertions(+), 11 deletions(-) diff --git a/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql b/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql index 85f9b98e1..c0dbbe892 100644 --- a/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql +++ b/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql @@ -4,13 +4,15 @@ CREATE TABLE "public"."workspace_calendar_sync_log" ( "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ws_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, - "google_account_id" text NOT NULL, + "access_token" text NOT NULL, + "refresh_token" text NOT NULL, "sync_started_at" timestamp with time zone NOT NULL, "sync_ended_at" timestamp with time zone, "status" text NOT NULL, "error_message" text, "event_snapshot_before" jsonb NOT NULL, - "event_snapshot_after" jsonb, + "upserted_events" jsonb, + "deleted_events" jsonb, "triggered_by" text NOT NULL, "created_at" timestamp with time zone NOT NULL DEFAULT now() ); @@ -24,7 +26,6 @@ ALTER TABLE "public"."workspace_calendar_sync_log" ADD CONSTRAINT "workspace_cal -- Create indexes for better query performance CREATE INDEX workspace_calendar_sync_log_workspace_id_idx ON public.workspace_calendar_sync_log USING btree (ws_id); -CREATE INDEX workspace_calendar_sync_log_google_account_id_idx ON public.workspace_calendar_sync_log USING btree (google_account_id); CREATE INDEX workspace_calendar_sync_log_status_idx ON public.workspace_calendar_sync_log USING btree (status); CREATE INDEX workspace_calendar_sync_log_sync_started_at_idx ON public.workspace_calendar_sync_log USING btree (sync_started_at); @@ -83,7 +84,7 @@ CHECK (status IN ('success', 'failed', 'in_progress', 'cancelled', 'partial_succ -- Add check constraint for triggered_by values ALTER TABLE "public"."workspace_calendar_sync_log" ADD CONSTRAINT "workspace_calendar_sync_log_triggered_by_check" -CHECK (triggered_by IN ('active_sync', 'trigger_dev', 'manual')); +CHECK (triggered_by IN ('active_sync', 'trigger_dot_dev', 'manual')); -- Add check constraint to ensure sync_ended_at is after sync_started_at when both are present ALTER TABLE "public"."workspace_calendar_sync_log" diff --git a/packages/trigger/google-calendar-sync.ts b/packages/trigger/google-calendar-sync.ts index 7a58db914..3e22ae82f 100644 --- a/packages/trigger/google-calendar-sync.ts +++ b/packages/trigger/google-calendar-sync.ts @@ -59,7 +59,60 @@ export const syncGoogleCalendarEvents = async () => { tokens.map((token) => token.ws_id) ); + const endSync = async ( + ws_id: string, + access_token: string, + refresh_token: string, + syncStartedAt: string, + syncEndedAt: string, + status: string, + errorMessage: string, + eventSnapshotBefore: WorkspaceCalendarEvent[], + upsertedEvents: WorkspaceCalendarEvent[], + deletedEvents: WorkspaceCalendarEvent[] + ) => { + await supabase.from('workspace_calendar_sync_log').insert({ + ws_id, + access_token, + refresh_token, + sync_started_at: syncStartedAt, + sync_ended_at: syncEndedAt, + status: status, + error_message: errorMessage, + event_snapshot_before: eventSnapshotBefore, + upserted_events: upsertedEvents, + deleted_events: deletedEvents, + triggered_by: 'trigger_dot_dev', + }); + }; + for (const token of tokens || []) { + const syncStartedAt = dayjs().toISOString(); + const { data: eventsBeforeSync, error: errorEventsBeforeSync } = + await supabase + .from('workspace_calendar_events') + .select('*') + .eq('ws_id', token.ws_id) + .not('google_event_id', 'is', null); + if (errorEventsBeforeSync) { + console.error( + 'Error fetching events before sync:', + errorEventsBeforeSync + ); + endSync( + token.ws_id, + token.access_token, + token.refresh_token, + syncStartedAt, + dayjs().toISOString(), + 'failed', + 'Error fetching events before sync: ' + errorEventsBeforeSync.message, + [], + [], + [] + ); + continue; + } const { ws_id, access_token, refresh_token } = token; if (!access_token) { console.error('No Google access token found for wsIds:', { @@ -67,6 +120,19 @@ export const syncGoogleCalendarEvents = async () => { hasAccessToken: !!access_token, hasRefreshToken: !!refresh_token, }); + endSync( + token.ws_id, + token.access_token, + token.refresh_token, + syncStartedAt, + dayjs().toISOString(), + 'failed', + 'No Google access token found for wsIds', + eventsBeforeSync || [], + [], + [] + ); + continue; } try { @@ -115,7 +181,18 @@ export const syncGoogleCalendarEvents = async () => { }); if (error) { console.error('Error upserting events:', error); - continue; + endSync( + ws_id, + access_token, + refresh_token, + syncStartedAt, + dayjs().toISOString(), + 'failed', + 'Error upserting events: ' + error.message, + eventsBeforeSync || [], + [], + [] + ); } console.log( 'Upserted events for wsId:', @@ -134,6 +211,18 @@ export const syncGoogleCalendarEvents = async () => { if (dbError) { console.error('Error fetching events after upsert:', dbError); + endSync( + ws_id, + access_token, + refresh_token, + syncStartedAt, + dayjs().toISOString(), + 'failed', + 'Error fetching events after upsert: ' + dbError.message, + eventsBeforeSync || [], + formattedEvents as WorkspaceCalendarEvent[], + [] + ); continue; } @@ -158,6 +247,18 @@ export const syncGoogleCalendarEvents = async () => { if (deleteError) { console.error('Error deleting events:', deleteError); + endSync( + ws_id, + access_token, + refresh_token, + syncStartedAt, + dayjs().toISOString(), + 'failed', + 'Error deleting events: ' + deleteError.message, + eventsBeforeSync || [], + formattedEvents as WorkspaceCalendarEvent[], + [] + ); continue; } @@ -167,10 +268,35 @@ export const syncGoogleCalendarEvents = async () => { eventsToDelete.map((e) => e.title) ); + endSync( + ws_id, + access_token, + refresh_token, + syncStartedAt, + dayjs().toISOString(), + 'success', + '', + eventsBeforeSync || [], + formattedEvents as WorkspaceCalendarEvent[], + eventsToDelete as WorkspaceCalendarEvent[] + ); + // Update lastUpsert timestamp after successful upsert await updateLastUpsert(ws_id, supabase); } catch (error) { console.error('Error fetching Google Calendar events:', error); + endSync( + ws_id, + access_token, + refresh_token, + syncStartedAt, + dayjs().toISOString(), + 'failed', + 'Error fetching Google Calendar events: ' + error, + eventsBeforeSync || [], + [], + [] + ); } } } catch (error) { diff --git a/packages/types/src/supabase.ts b/packages/types/src/supabase.ts index d5921fe18..25961093a 100644 --- a/packages/types/src/supabase.ts +++ b/packages/types/src/supabase.ts @@ -5064,42 +5064,48 @@ export type Database = { }; workspace_calendar_sync_log: { Row: { + access_token: string; created_at: string; + deleted_events: Json | null; error_message: string | null; - event_snapshot_after: Json | null; event_snapshot_before: Json; - google_account_id: string; id: string; + refresh_token: string; status: string; sync_ended_at: string | null; sync_started_at: string; triggered_by: string; + upserted_events: Json | null; ws_id: string; }; Insert: { + access_token: string; created_at?: string; + deleted_events?: Json | null; error_message?: string | null; - event_snapshot_after?: Json | null; event_snapshot_before: Json; - google_account_id: string; id?: string; + refresh_token: string; status: string; sync_ended_at?: string | null; sync_started_at: string; triggered_by: string; + upserted_events?: Json | null; ws_id: string; }; Update: { + access_token?: string; created_at?: string; + deleted_events?: Json | null; error_message?: string | null; - event_snapshot_after?: Json | null; event_snapshot_before?: Json; - google_account_id?: string; id?: string; + refresh_token?: string; status?: string; sync_ended_at?: string | null; sync_started_at?: string; triggered_by?: string; + upserted_events?: Json | null; ws_id?: string; }; Relationships: [ From f0b4fcbb6b779831a46c404bf416c8270b0006b3 Mon Sep 17 00:00:00 2001 From: DennieDan Date: Fri, 20 Jun 2025 19:57:13 +0800 Subject: [PATCH 3/5] fix: log gg email instead of token for security --- ...134409_add_workspace_calendar_sync_log.sql | 3 +- packages/trigger/google-calendar-sync.ts | 73 ++++++++++++------- packages/types/src/supabase.ts | 9 +-- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql b/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql index c0dbbe892..fe4109c8a 100644 --- a/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql +++ b/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql @@ -4,8 +4,7 @@ CREATE TABLE "public"."workspace_calendar_sync_log" ( "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ws_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, - "access_token" text NOT NULL, - "refresh_token" text NOT NULL, + "google_account_email" text, "sync_started_at" timestamp with time zone NOT NULL, "sync_ended_at" timestamp with time zone, "status" text NOT NULL, diff --git a/packages/trigger/google-calendar-sync.ts b/packages/trigger/google-calendar-sync.ts index 3e22ae82f..c1bae013a 100644 --- a/packages/trigger/google-calendar-sync.ts +++ b/packages/trigger/google-calendar-sync.ts @@ -61,8 +61,7 @@ export const syncGoogleCalendarEvents = async () => { const endSync = async ( ws_id: string, - access_token: string, - refresh_token: string, + googleAccountEmail: string | null, syncStartedAt: string, syncEndedAt: string, status: string, @@ -73,8 +72,7 @@ export const syncGoogleCalendarEvents = async () => { ) => { await supabase.from('workspace_calendar_sync_log').insert({ ws_id, - access_token, - refresh_token, + google_account_email: googleAccountEmail, sync_started_at: syncStartedAt, sync_ended_at: syncEndedAt, status: status, @@ -88,6 +86,34 @@ export const syncGoogleCalendarEvents = async () => { for (const token of tokens || []) { const syncStartedAt = dayjs().toISOString(); + let googleAccountEmail: string | null = null; + + const auth = getGoogleAuthClient(token); + + try { + // get google account email from google api + const googleAccount = await auth.getTokenInfo( + token.access_token as string + ); + googleAccountEmail = googleAccount.email || null; + console.log('googleAccountEmail', googleAccountEmail); + } catch (error) { + console.error('Error fetching google account email:', error); + await endSync( + token.ws_id, + null, + syncStartedAt, + dayjs().toISOString(), + 'failed', + 'Error fetching google account email: ' + error, + [], + [], + [] + ); + continue; + } + + // get events before sync const { data: eventsBeforeSync, error: errorEventsBeforeSync } = await supabase .from('workspace_calendar_events') @@ -99,15 +125,14 @@ export const syncGoogleCalendarEvents = async () => { 'Error fetching events before sync:', errorEventsBeforeSync ); - endSync( + await endSync( token.ws_id, - token.access_token, - token.refresh_token, + googleAccountEmail, syncStartedAt, dayjs().toISOString(), 'failed', 'Error fetching events before sync: ' + errorEventsBeforeSync.message, - [], + eventsBeforeSync || [], [], [] ); @@ -120,10 +145,9 @@ export const syncGoogleCalendarEvents = async () => { hasAccessToken: !!access_token, hasRefreshToken: !!refresh_token, }); - endSync( + await endSync( token.ws_id, - token.access_token, - token.refresh_token, + googleAccountEmail, syncStartedAt, dayjs().toISOString(), 'failed', @@ -136,7 +160,6 @@ export const syncGoogleCalendarEvents = async () => { } try { - const auth = getGoogleAuthClient(token); const calendar = google.calendar({ version: 'v3', auth }); const startOfCurrentWeek = dayjs().startOf('week'); @@ -181,10 +204,9 @@ export const syncGoogleCalendarEvents = async () => { }); if (error) { console.error('Error upserting events:', error); - endSync( + await endSync( ws_id, - access_token, - refresh_token, + googleAccountEmail, syncStartedAt, dayjs().toISOString(), 'failed', @@ -193,6 +215,7 @@ export const syncGoogleCalendarEvents = async () => { [], [] ); + continue; } console.log( 'Upserted events for wsId:', @@ -211,10 +234,9 @@ export const syncGoogleCalendarEvents = async () => { if (dbError) { console.error('Error fetching events after upsert:', dbError); - endSync( + await endSync( ws_id, - access_token, - refresh_token, + googleAccountEmail, syncStartedAt, dayjs().toISOString(), 'failed', @@ -247,10 +269,9 @@ export const syncGoogleCalendarEvents = async () => { if (deleteError) { console.error('Error deleting events:', deleteError); - endSync( + await endSync( ws_id, - access_token, - refresh_token, + googleAccountEmail, syncStartedAt, dayjs().toISOString(), 'failed', @@ -268,10 +289,9 @@ export const syncGoogleCalendarEvents = async () => { eventsToDelete.map((e) => e.title) ); - endSync( + await endSync( ws_id, - access_token, - refresh_token, + googleAccountEmail, syncStartedAt, dayjs().toISOString(), 'success', @@ -285,10 +305,9 @@ export const syncGoogleCalendarEvents = async () => { await updateLastUpsert(ws_id, supabase); } catch (error) { console.error('Error fetching Google Calendar events:', error); - endSync( + await endSync( ws_id, - access_token, - refresh_token, + googleAccountEmail, syncStartedAt, dayjs().toISOString(), 'failed', diff --git a/packages/types/src/supabase.ts b/packages/types/src/supabase.ts index 25961093a..647300cd9 100644 --- a/packages/types/src/supabase.ts +++ b/packages/types/src/supabase.ts @@ -5064,13 +5064,12 @@ export type Database = { }; workspace_calendar_sync_log: { Row: { - access_token: string; created_at: string; deleted_events: Json | null; error_message: string | null; event_snapshot_before: Json; + google_account_email: string | null; id: string; - refresh_token: string; status: string; sync_ended_at: string | null; sync_started_at: string; @@ -5079,13 +5078,12 @@ export type Database = { ws_id: string; }; Insert: { - access_token: string; created_at?: string; deleted_events?: Json | null; error_message?: string | null; event_snapshot_before: Json; + google_account_email?: string | null; id?: string; - refresh_token: string; status: string; sync_ended_at?: string | null; sync_started_at: string; @@ -5094,13 +5092,12 @@ export type Database = { ws_id: string; }; Update: { - access_token?: string; created_at?: string; deleted_events?: Json | null; error_message?: string | null; event_snapshot_before?: Json; + google_account_email?: string | null; id?: string; - refresh_token?: string; status?: string; sync_ended_at?: string | null; sync_started_at?: string; From 2627fd8994956649d352906fe7208fddf87fdb25 Mon Sep 17 00:00:00 2001 From: DennieDan Date: Fri, 20 Jun 2025 20:02:44 +0800 Subject: [PATCH 4/5] feat: add tobe upserted and to be deleted events --- packages/trigger/google-calendar-sync.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/trigger/google-calendar-sync.ts b/packages/trigger/google-calendar-sync.ts index c1bae013a..5093a4e28 100644 --- a/packages/trigger/google-calendar-sync.ts +++ b/packages/trigger/google-calendar-sync.ts @@ -212,7 +212,7 @@ export const syncGoogleCalendarEvents = async () => { 'failed', 'Error upserting events: ' + error.message, eventsBeforeSync || [], - [], + formattedEvents as WorkspaceCalendarEvent[], [] ); continue; @@ -278,7 +278,7 @@ export const syncGoogleCalendarEvents = async () => { 'Error deleting events: ' + deleteError.message, eventsBeforeSync || [], formattedEvents as WorkspaceCalendarEvent[], - [] + eventsToDelete as WorkspaceCalendarEvent[] ); continue; } From e3d15bf0bbfef1269839d76b2adeecdeb81b4855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=B5=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Fri, 20 Jun 2025 21:19:55 +0700 Subject: [PATCH 5/5] fix(db): remove redundant RLS permissions --- ...134409_add_workspace_calendar_sync_log.sql | 33 --- packages/types/src/supabase.ts | 191 +++++++++--------- 2 files changed, 95 insertions(+), 129 deletions(-) diff --git a/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql b/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql index fe4109c8a..f1dd1ba13 100644 --- a/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql +++ b/apps/db/supabase/migrations/20250620134409_add_workspace_calendar_sync_log.sql @@ -42,39 +42,6 @@ USING ( ) ); --- Users can insert sync logs for workspaces they are members of -CREATE POLICY "Users can insert sync logs for their workspaces" -ON public.workspace_calendar_sync_log -FOR INSERT -TO authenticated -WITH CHECK ( - EXISTS ( - SELECT 1 FROM public.workspace_members wm - WHERE wm.ws_id = ws_id - AND wm.user_id = auth.uid() - ) -); - --- Users can update sync logs for workspaces they are members of -CREATE POLICY "Users can update sync logs for their workspaces" -ON public.workspace_calendar_sync_log -FOR UPDATE -TO authenticated -USING ( - EXISTS ( - SELECT 1 FROM public.workspace_members wm - WHERE wm.ws_id = ws_id - AND wm.user_id = auth.uid() - ) -) -WITH CHECK ( - EXISTS ( - SELECT 1 FROM public.workspace_members wm - WHERE wm.ws_id = ws_id - AND wm.user_id = auth.uid() - ) -); - -- Add check constraint for status values ALTER TABLE "public"."workspace_calendar_sync_log" ADD CONSTRAINT "workspace_calendar_sync_log_status_check" diff --git a/packages/types/src/supabase.ts b/packages/types/src/supabase.ts index 040079927..e13ae1580 100644 --- a/packages/types/src/supabase.ts +++ b/packages/types/src/supabase.ts @@ -7732,7 +7732,7 @@ export type Database = { }; Functions: { calculate_productivity_score: { - Args: { category_color: string; duration_seconds: number }; + Args: { duration_seconds: number; category_color: string }; Returns: number; }; check_ws_creator: { @@ -7749,12 +7749,12 @@ export type Database = { }; count_search_users: { Args: - | { search_query: string } | { - search_query: string; - role_filter?: string; enabled_filter?: boolean; - }; + role_filter?: string; + search_query: string; + } + | { search_query: string }; Returns: number; }; create_ai_chat: { @@ -7764,34 +7764,33 @@ export type Database = { generate_cross_app_token: { Args: | { - p_expiry_seconds?: number; - p_target_app: string; - p_origin_app: string; p_user_id: string; + p_origin_app: string; + p_target_app: string; + p_expiry_seconds?: number; } | { + p_user_id: string; p_session_data?: Json; - p_target_app: string; p_expiry_seconds?: number; + p_target_app: string; p_origin_app: string; - p_user_id: string; }; Returns: string; }; get_challenge_stats: { - Args: { challenge_id_param: string; user_id_param: string }; + Args: { user_id_param: string; challenge_id_param: string }; Returns: { total_score: number; problems_attempted: number; }[]; }; get_daily_income_expense: { - Args: { _ws_id: string; past_days?: number }; + Args: { past_days?: number; _ws_id: string }; Returns: { - total_income: number; day: string; - total_income: number; total_expense: number; + total_income: number; }[]; }; get_daily_prompt_completion_tokens: { @@ -7825,9 +7824,9 @@ export type Database = { get_hourly_prompt_completion_tokens: { Args: { past_hours?: number }; Returns: { - total_completion_tokens: number; hour: string; total_prompt_tokens: number; + total_completion_tokens: number; }[]; }; get_inventory_batches_count: { @@ -7841,21 +7840,21 @@ export type Database = { get_inventory_products: { Args: { _has_unit?: boolean; - _category_ids?: string[]; - _ws_id?: string; _warehouse_ids?: string[]; + _ws_id?: string; + _category_ids?: string[]; }; Returns: { - created_at: string; - ws_id: string; - amount: number; - price: number; - category: string; unit_id: string; - unit: string; + category: string; + price: number; + amount: number; + ws_id: string; + created_at: string; manufacturer: string; - id: string; name: string; + id: string; + unit: string; }[]; }; get_inventory_products_count: { @@ -7875,7 +7874,7 @@ export type Database = { Returns: number; }; get_monthly_income_expense: { - Args: { _ws_id: string; past_months?: number }; + Args: { past_months?: number; _ws_id: string }; Returns: { total_expense: number; total_income: number; @@ -7895,117 +7894,117 @@ export type Database = { Returns: number; }; get_possible_excluded_groups: { - Args: { _ws_id: string; included_groups: string[] }; + Args: { included_groups: string[]; _ws_id: string }; Returns: { - id: string; name: string; - ws_id: string; + id: string; amount: number; + ws_id: string; }[]; }; get_possible_excluded_tags: { Args: { _ws_id: string; included_tags: string[] }; Returns: { - id: string; - name: string; - ws_id: string; amount: number; + ws_id: string; + name: string; + id: string; }[]; }; get_session_statistics: { Args: Record; Returns: { - unique_users_count: number; - completed_count: number; + active_count: number; latest_session_date: string; + completed_count: number; total_count: number; - active_count: number; + unique_users_count: number; }[]; }; get_session_templates: { Args: { limit_count?: number; - workspace_id: string; user_id_param: string; + workspace_id: string; }; Returns: { + description: string; + category_id: string; + task_id: string; + tags: string[]; + category_name: string; category_color: string; task_name: string; usage_count: number; avg_duration: number; last_used: string; - task_id: string; - category_name: string; - category_id: string; - tags: string[]; title: string; - description: string; }[]; }; get_submission_statistics: { Args: Record; Returns: { - unique_users_count: number; latest_submission_date: string; total_count: number; + unique_users_count: number; }[]; }; get_transaction_categories_with_amount: { Args: Record; Returns: { - name: string; + is_expense: boolean; ws_id: string; created_at: string; amount: number; id: string; - is_expense: boolean; + name: string; }[]; }; get_user_role: { - Args: { user_id: string; ws_id: string }; + Args: { ws_id: string; user_id: string }; Returns: string; }; get_user_session_stats: { Args: { user_id: string }; Returns: { + active_sessions: number; current_session_age: unknown; total_sessions: number; - active_sessions: number; }[]; }; get_user_sessions: { Args: { user_id: string }; Returns: { - is_current: boolean; - session_id: string; created_at: string; updated_at: string; user_agent: string; ip: string; + is_current: boolean; + session_id: string; }[]; }; get_user_tasks: { Args: { _board_id: string }; Returns: { - name: string; - end_date: string; - start_date: string; - completed: boolean; - priority: number; - description: string; - id: string; board_id: string; + description: string; + priority: number; + completed: boolean; + start_date: string; + end_date: string; list_id: string; + id: string; + name: string; }[]; }; get_user_whitelist_status: { Args: { user_id_param: string }; Returns: { - allow_role_management: boolean; - allow_challenge_management: boolean; + is_whitelisted: boolean; allow_manage_all_challenges: boolean; + allow_challenge_management: boolean; + allow_role_management: boolean; enabled: boolean; - is_whitelisted: boolean; }[]; }; get_workspace_drive_size: { @@ -8021,20 +8020,20 @@ export type Database = { Returns: number; }; get_workspace_transactions_count: { - Args: { ws_id: string; start_date?: string; end_date?: string }; + Args: { end_date?: string; start_date?: string; ws_id: string }; Returns: number; }; get_workspace_user_groups: { Args: { _ws_id: string; - included_tags: string[]; - excluded_tags: string[]; search_query: string; + excluded_tags: string[]; + included_tags: string[]; }; Returns: { - id: string; - name: string; notes: string; + name: string; + id: string; ws_id: string; tags: string[]; tag_count: number; @@ -8047,32 +8046,32 @@ export type Database = { }; get_workspace_users: { Args: { - _ws_id: string; included_groups: string[]; + _ws_id: string; excluded_groups: string[]; search_query: string; }; Returns: { + id: string; + avatar_url: string; + full_name: string; + display_name: string; email: string; + phone: string; + gender: string; + birthday: string; + ethnicity: string; + guardian: string; + address: string; + national_id: string; + note: string; + balance: number; + ws_id: string; groups: string[]; - full_name: string; - avatar_url: string; - id: string; + group_count: number; linked_users: Json; created_at: string; updated_at: string; - phone: string; - ws_id: string; - balance: number; - note: string; - national_id: string; - address: string; - guardian: string; - ethnicity: string; - birthday: string; - gender: string; - display_name: string; - group_count: number; }[]; }; get_workspace_users_count: { @@ -8088,15 +8087,15 @@ export type Database = { Returns: number; }; get_workspace_wallets_income: { - Args: { ws_id: string; start_date?: string; end_date?: string }; + Args: { end_date?: string; start_date?: string; ws_id: string }; Returns: number; }; has_other_owner: { - Args: { _ws_id: string; _user_id: string }; + Args: { _user_id: string; _ws_id: string }; Returns: boolean; }; insert_ai_chat_message: { - Args: { message: string; chat_id: string; source: string }; + Args: { source: string; message: string; chat_id: string }; Returns: undefined; }; is_list_accessible: { @@ -8116,11 +8115,11 @@ export type Database = { Returns: boolean; }; is_nova_user_email_in_team: { - Args: { _user_email: string; _team_id: string }; + Args: { _team_id: string; _user_email: string }; Returns: boolean; }; is_nova_user_id_in_team: { - Args: { _user_id: string; _team_id: string }; + Args: { _team_id: string; _user_id: string }; Returns: boolean; }; is_org_member: { @@ -8136,7 +8135,7 @@ export type Database = { Returns: boolean; }; is_task_board_member: { - Args: { _board_id: string; _user_id: string }; + Args: { _user_id: string; _board_id: string }; Returns: boolean; }; is_user_task_in_board: { @@ -8156,11 +8155,11 @@ export type Database = { Returns: Json; }; nova_get_user_daily_sessions: { - Args: { challenge_id: string; user_id: string }; + Args: { user_id: string; challenge_id: string }; Returns: number; }; nova_get_user_total_sessions: { - Args: { challenge_id: string; user_id: string }; + Args: { user_id: string; challenge_id: string }; Returns: number; }; revoke_all_cross_app_tokens: { @@ -8186,8 +8185,6 @@ export type Database = { enabled_filter?: boolean; }; Returns: { - created_at: string; - enabled: boolean; allow_challenge_management: boolean; allow_manage_all_challenges: boolean; allow_role_management: boolean; @@ -8195,27 +8192,29 @@ export type Database = { new_email: string; birthday: string; team_name: string[]; - user_id: string; id: string; display_name: string; deleted: boolean; avatar_url: string; handle: string; bio: string; + created_at: string; + user_id: string; + enabled: boolean; }[]; }; search_users_by_name: { Args: { - min_similarity?: number; - result_limit?: number; search_query: string; + result_limit?: number; + min_similarity?: number; }; Returns: { handle: string; - id: string; - display_name: string; - avatar_url: string; relevance: number; + avatar_url: string; + display_name: string; + id: string; }[]; }; sum_quiz_scores: { @@ -8229,7 +8228,7 @@ export type Database = { Returns: boolean; }; transactions_have_same_amount: { - Args: { transaction_id_1: string; transaction_id_2: string }; + Args: { transaction_id_2: string; transaction_id_1: string }; Returns: boolean; }; update_expired_sessions: { @@ -8237,7 +8236,7 @@ export type Database = { Returns: undefined; }; update_session_total_score: { - Args: { user_id_param: string; challenge_id_param: string }; + Args: { challenge_id_param: string; user_id_param: string }; Returns: undefined; }; validate_cross_app_token: {