From 7f7a6b06d28350ea85e437b7fc79f2e45474be1f Mon Sep 17 00:00:00 2001 From: Georgios Metaxakis Date: Thu, 5 Mar 2026 22:34:28 +0200 Subject: [PATCH 1/4] feat: add generated Supabase TypeScript types and typed client --- package.json | 1 + src/database.types.ts | 1404 +++++++++++++++++++++++++++++ src/repository/supabase.server.ts | 3 +- 3 files changed, 1407 insertions(+), 1 deletion(-) create mode 100644 src/database.types.ts diff --git a/package.json b/package.json index aea79be975..91ae6c43ac 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "storybook": "bun run --filter oa-components start", "storybook:build": "bun run build:shared && bun run build:themes && bun run --filter oa-components build:sb", "docs": "bun run --filter oa-docs start", + "db:types": "supabase gen types --local > src/database.types.ts", "db:reset": "npx supabase db reset && bun run db:seed", "db:seed": "npx @snaplet/seed sync && npx tsx seed.ts > seed.sql", "commit": "git-cz", diff --git a/src/database.types.ts b/src/database.types.ts new file mode 100644 index 0000000000..c35c5e37fd --- /dev/null +++ b/src/database.types.ts @@ -0,0 +1,1404 @@ +export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]; + +export type Database = { + public: { + Tables: { + banners: { + Row: { + id: number; + created_at: string; + modified_at: string | null; + text: string; + url: string | null; + tenant_id: string; + }; + Insert: { + id?: number; + created_at?: string; + modified_at?: string | null; + text: string; + url?: string | null; + tenant_id: string; + }; + Update: { + id?: number; + created_at?: string; + modified_at?: string | null; + text?: string; + url?: string | null; + tenant_id?: string; + }; + Relationships: []; + }; + categories: { + Row: { + id: number; + created_at: string; + tenant_id: string; + name: string; + legacy_id: string | null; + type: Database['public']['Enums']['content_types'] | null; + }; + Insert: { + id?: number; + created_at?: string; + tenant_id: string; + name: string; + legacy_id?: string | null; + type?: Database['public']['Enums']['content_types'] | null; + }; + Update: { + id?: number; + created_at?: string; + tenant_id?: string; + name?: string; + legacy_id?: string | null; + type?: Database['public']['Enums']['content_types'] | null; + }; + Relationships: []; + }; + comments: { + Row: { + id: number; + created_at: string; + comment: string; + source_id: number | null; + parent_id: number | null; + tenant_id: string; + created_by: number | null; + source_type: string; + modified_at: string | null; + source_id_legacy: string | null; + deleted: boolean | null; + legacy_id: string | null; + }; + Insert: { + id?: number; + created_at?: string; + comment: string; + source_id?: number | null; + parent_id?: number | null; + tenant_id?: string; + created_by?: number | null; + source_type: string; + modified_at?: string | null; + source_id_legacy?: string | null; + deleted?: boolean | null; + legacy_id?: string | null; + }; + Update: { + id?: number; + created_at?: string; + comment?: string; + source_id?: number | null; + parent_id?: number | null; + tenant_id?: string; + created_by?: number | null; + source_type?: string; + modified_at?: string | null; + source_id_legacy?: string | null; + deleted?: boolean | null; + legacy_id?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'comments_created_by_fkey'; + columns: ['created_by']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + ]; + }; + map_pins: { + Row: { + id: number; + created_at: string; + profile_id: number; + country: string; + country_code: string; + administrative: string | null; + post_code: string | null; + lat: string; + lng: string; + moderation: string; + tenant_id: string; + moderation_feedback: string | null; + name: string | null; + }; + Insert: { + id?: number; + created_at?: string; + profile_id: number; + country: string; + country_code: string; + administrative?: string | null; + post_code?: string | null; + lat: string; + lng: string; + moderation: string; + tenant_id: string; + moderation_feedback?: string | null; + name?: string | null; + }; + Update: { + id?: number; + created_at?: string; + profile_id?: number; + country?: string; + country_code?: string; + administrative?: string | null; + post_code?: string | null; + lat?: string; + lng?: string; + moderation?: string; + tenant_id?: string; + moderation_feedback?: string | null; + name?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'map_pins_profile_id_fkey'; + columns: ['profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + ]; + }; + map_settings: { + Row: { + id: number; + default_type_filters: string[] | null; + setting_filters: string[]; + tenant_id: string; + }; + Insert: { + id?: number; + default_type_filters?: string[] | null; + setting_filters: string[]; + tenant_id: string; + }; + Update: { + id?: number; + default_type_filters?: string[] | null; + setting_filters?: string[]; + tenant_id?: string; + }; + Relationships: []; + }; + messages: { + Row: { + id: number; + created_at: string; + message: string; + sender_id: number; + receiver_id: number | null; + tenant_id: string; + }; + Insert: { + id?: number; + created_at?: string; + message: string; + sender_id: number; + receiver_id?: number | null; + tenant_id: string; + }; + Update: { + id?: number; + created_at?: string; + message?: string; + sender_id?: number; + receiver_id?: number | null; + tenant_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'messages_sender_id_fkey'; + columns: ['sender_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'messages_receiver_id_fkey'; + columns: ['receiver_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + ]; + }; + news: { + Row: { + id: number; + created_at: string; + created_by: number | null; + deleted: boolean | null; + modified_at: string | null; + comment_count: number; + body: string; + moderation: string | null; + slug: string; + previous_slugs: string[] | null; + category: number | null; + tags: number[] | null; + title: string; + total_views: number | null; + tenant_id: string; + hero_image: Json | null; + summary: string | null; + is_draft: boolean; + profile_badge: number | null; + }; + Insert: { + id?: number; + created_at?: string; + created_by?: number | null; + deleted?: boolean | null; + modified_at?: string | null; + comment_count?: number; + body: string; + moderation?: string | null; + slug: string; + previous_slugs?: string[] | null; + category?: number | null; + tags?: number[] | null; + title: string; + total_views?: number | null; + tenant_id: string; + hero_image?: Json | null; + summary?: string | null; + is_draft?: boolean; + profile_badge?: number | null; + }; + Update: { + id?: number; + created_at?: string; + created_by?: number | null; + deleted?: boolean | null; + modified_at?: string | null; + comment_count?: number; + body?: string; + moderation?: string | null; + slug?: string; + previous_slugs?: string[] | null; + category?: number | null; + tags?: number[] | null; + title?: string; + total_views?: number | null; + tenant_id?: string; + hero_image?: Json | null; + summary?: string | null; + is_draft?: boolean; + profile_badge?: number | null; + }; + Relationships: [ + { + foreignKeyName: 'news_created_by_fkey'; + columns: ['created_by']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'news_category_fkey'; + columns: ['category']; + isOneToOne: false; + referencedRelation: 'categories'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'news_profile_badge_fkey'; + columns: ['profile_badge']; + isOneToOne: false; + referencedRelation: 'profile_badges'; + referencedColumns: ['id']; + }, + ]; + }; + notifications: { + Row: { + id: number; + created_at: string; + modified_at: string; + title: string | null; + owned_by_id: number; + triggered_by_id: number; + content_type: Database['public']['Enums']['notification_content_types']; + content_id: number; + is_read: boolean; + action_type: Database['public']['Enums']['notification_action_types']; + tenant_id: string; + source_content_type: string | null; + source_content_id: number | null; + parent_comment_id: number | null; + parent_content_id: number | null; + should_email: boolean | null; + }; + Insert: { + id?: number; + created_at?: string; + modified_at?: string; + title?: string | null; + owned_by_id: number; + triggered_by_id: number; + content_type: Database['public']['Enums']['notification_content_types']; + content_id: number; + is_read?: boolean; + action_type: Database['public']['Enums']['notification_action_types']; + tenant_id: string; + source_content_type?: string | null; + source_content_id?: number | null; + parent_comment_id?: number | null; + parent_content_id?: number | null; + should_email?: boolean | null; + }; + Update: { + id?: number; + created_at?: string; + modified_at?: string; + title?: string | null; + owned_by_id?: number; + triggered_by_id?: number; + content_type?: Database['public']['Enums']['notification_content_types']; + content_id?: number; + is_read?: boolean; + action_type?: Database['public']['Enums']['notification_action_types']; + tenant_id?: string; + source_content_type?: string | null; + source_content_id?: number | null; + parent_comment_id?: number | null; + parent_content_id?: number | null; + should_email?: boolean | null; + }; + Relationships: [ + { + foreignKeyName: 'notifications_owned_by_id_fkey'; + columns: ['owned_by_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'notifications_triggered_by_id_fkey'; + columns: ['triggered_by_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + ]; + }; + notifications_preferences: { + Row: { + id: number; + user_id: number; + comments: boolean; + replies: boolean; + tenant_id: string; + research_updates: boolean; + is_unsubscribed: boolean; + }; + Insert: { + id?: number; + user_id: number; + comments: boolean; + replies: boolean; + tenant_id: string; + research_updates: boolean; + is_unsubscribed?: boolean; + }; + Update: { + id?: number; + user_id?: number; + comments?: boolean; + replies?: boolean; + tenant_id?: string; + research_updates?: boolean; + is_unsubscribed?: boolean; + }; + Relationships: [ + { + foreignKeyName: 'notifications_preferences_user_id_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + ]; + }; + patreon_settings: { + Row: { + id: number; + created_at: string; + tiers: Json | null; + tenant_id: string | null; + }; + Insert: { + id?: number; + created_at?: string; + tiers?: Json | null; + tenant_id?: string | null; + }; + Update: { + id?: number; + created_at?: string; + tiers?: Json | null; + tenant_id?: string | null; + }; + Relationships: []; + }; + profile_badges: { + Row: { + id: number; + name: string; + display_name: string; + image_url: string; + action_url: string | null; + tenant_id: string; + premium_tier: number | null; + }; + Insert: { + id?: number; + name: string; + display_name: string; + image_url: string; + action_url?: string | null; + tenant_id: string; + premium_tier?: number | null; + }; + Update: { + id?: number; + name?: string; + display_name?: string; + image_url?: string; + action_url?: string | null; + tenant_id?: string; + premium_tier?: number | null; + }; + Relationships: []; + }; + profile_badges_relations: { + Row: { + id: number; + profile_id: number; + profile_badge_id: number; + created_at: string; + tenant_id: string; + }; + Insert: { + id?: number; + profile_id: number; + profile_badge_id: number; + created_at?: string; + tenant_id: string; + }; + Update: { + id?: number; + profile_id?: number; + profile_badge_id?: number; + created_at?: string; + tenant_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'profile_badges_relations_profile_id_fkey'; + columns: ['profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'profile_badges_relations_profile_badge_id_fkey'; + columns: ['profile_badge_id']; + isOneToOne: false; + referencedRelation: 'profile_badges'; + referencedColumns: ['id']; + }, + ]; + }; + profile_tags: { + Row: { + id: number; + created_at: string; + name: string; + tenant_id: string; + profile_type: string | null; + }; + Insert: { + id?: number; + created_at?: string; + name: string; + tenant_id: string; + profile_type?: string | null; + }; + Update: { + id?: number; + created_at?: string; + name?: string; + tenant_id?: string; + profile_type?: string | null; + }; + Relationships: []; + }; + profile_tags_relations: { + Row: { + id: number; + created_at: string; + profile_id: number; + profile_tag_id: number; + tenant_id: string; + }; + Insert: { + id?: number; + created_at?: string; + profile_id: number; + profile_tag_id: number; + tenant_id: string; + }; + Update: { + id?: number; + created_at?: string; + profile_id?: number; + profile_tag_id?: number; + tenant_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'profile_tags_relations_profile_id_fkey'; + columns: ['profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'profile_tags_relations_profile_tag_id_fkey'; + columns: ['profile_tag_id']; + isOneToOne: false; + referencedRelation: 'profile_tags'; + referencedColumns: ['id']; + }, + ]; + }; + profile_types: { + Row: { + id: number; + name: string; + display_name: string; + order: number; + image_url: string; + small_image_url: string; + description: string; + map_pin_name: string; + is_space: boolean; + tenant_id: string; + }; + Insert: { + id?: number; + name: string; + display_name: string; + order: number; + image_url: string; + small_image_url: string; + description: string; + map_pin_name: string; + is_space: boolean; + tenant_id: string; + }; + Update: { + id?: number; + name?: string; + display_name?: string; + order?: number; + image_url?: string; + small_image_url?: string; + description?: string; + map_pin_name?: string; + is_space?: boolean; + tenant_id?: string; + }; + Relationships: []; + }; + profiles: { + Row: { + id: number; + created_at: string; + firebase_auth_id: string | null; + display_name: string; + country: string | null; + about: string | null; + tenant_id: string; + username: string; + roles: string[] | null; + impact: Json | null; + is_blocked_from_messaging: boolean | null; + is_contactable: boolean | null; + is_supporter: boolean | null; + patreon: Json | null; + total_views: number | null; + type: string | null; + auth_id: string | null; + legacy_id: string | null; + cover_images: Json[] | null; + last_active: string | null; + photo: Json | null; + visitor_policy: Json | null; + website: string | null; + profile_type: number | null; + donations_enabled: boolean; + }; + Insert: { + id?: number; + created_at?: string; + firebase_auth_id?: string | null; + display_name: string; + country?: string | null; + about?: string | null; + tenant_id: string; + username?: string; + roles?: string[] | null; + impact?: Json | null; + is_blocked_from_messaging?: boolean | null; + is_contactable?: boolean | null; + is_supporter?: boolean | null; + patreon?: Json | null; + total_views?: number | null; + type?: string | null; + auth_id?: string | null; + legacy_id?: string | null; + cover_images?: Json[] | null; + last_active?: string | null; + photo?: Json | null; + visitor_policy?: Json | null; + website?: string | null; + profile_type?: number | null; + donations_enabled?: boolean; + }; + Update: { + id?: number; + created_at?: string; + firebase_auth_id?: string | null; + display_name?: string; + country?: string | null; + about?: string | null; + tenant_id?: string; + username?: string; + roles?: string[] | null; + impact?: Json | null; + is_blocked_from_messaging?: boolean | null; + is_contactable?: boolean | null; + is_supporter?: boolean | null; + patreon?: Json | null; + total_views?: number | null; + type?: string | null; + auth_id?: string | null; + legacy_id?: string | null; + cover_images?: Json[] | null; + last_active?: string | null; + photo?: Json | null; + visitor_policy?: Json | null; + website?: string | null; + profile_type?: number | null; + donations_enabled?: boolean; + }; + Relationships: [ + { + foreignKeyName: 'profiles_profile_type_fkey'; + columns: ['profile_type']; + isOneToOne: false; + referencedRelation: 'profile_types'; + referencedColumns: ['id']; + }, + ]; + }; + projects: { + Row: { + id: number; + created_at: string; + modified_at: string | null; + title: string; + slug: string; + previous_slugs: string[] | null; + description: string; + created_by: number | null; + deleted: boolean | null; + category: number | null; + difficulty_level: string | null; + cover_image: Json | null; + file_link: string | null; + files: Json[] | null; + tags: string[] | null; + is_draft: boolean | null; + time: string | null; + file_download_count: number | null; + moderation: string | null; + moderation_feedback: string | null; + tenant_id: string; + total_views: number | null; + comment_count: number | null; + legacy_id: string | null; + }; + Insert: { + id?: number; + created_at?: string; + modified_at?: string | null; + title: string; + slug: string; + previous_slugs?: string[] | null; + description: string; + created_by?: number | null; + deleted?: boolean | null; + category?: number | null; + difficulty_level?: string | null; + cover_image?: Json | null; + file_link?: string | null; + files?: Json[] | null; + tags?: string[] | null; + is_draft?: boolean | null; + time?: string | null; + file_download_count?: number | null; + moderation?: string | null; + moderation_feedback?: string | null; + tenant_id: string; + total_views?: number | null; + comment_count?: number | null; + legacy_id?: string | null; + }; + Update: { + id?: number; + created_at?: string; + modified_at?: string | null; + title?: string; + slug?: string; + previous_slugs?: string[] | null; + description?: string; + created_by?: number | null; + deleted?: boolean | null; + category?: number | null; + difficulty_level?: string | null; + cover_image?: Json | null; + file_link?: string | null; + files?: Json[] | null; + tags?: string[] | null; + is_draft?: boolean | null; + time?: string | null; + file_download_count?: number | null; + moderation?: string | null; + moderation_feedback?: string | null; + tenant_id?: string; + total_views?: number | null; + comment_count?: number | null; + legacy_id?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'projects_created_by_fkey'; + columns: ['created_by']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'projects_category_fkey'; + columns: ['category']; + isOneToOne: false; + referencedRelation: 'categories'; + referencedColumns: ['id']; + }, + ]; + }; + project_steps: { + Row: { + id: number; + created_at: string; + project_id: number; + title: string; + description: string; + images: Json | null; + video_url: string | null; + order: number | null; + tenant_id: string; + }; + Insert: { + id?: number; + created_at?: string; + project_id: number; + title: string; + description: string; + images?: Json | null; + video_url?: string | null; + order?: number | null; + tenant_id: string; + }; + Update: { + id?: number; + created_at?: string; + project_id?: number; + title?: string; + description?: string; + images?: Json | null; + video_url?: string | null; + order?: number | null; + tenant_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'project_steps_project_id_fkey'; + columns: ['project_id']; + isOneToOne: false; + referencedRelation: 'projects'; + referencedColumns: ['id']; + }, + ]; + }; + questions: { + Row: { + id: number; + created_at: string; + created_by: number | null; + deleted: boolean | null; + modified_at: string; + comment_count: number; + description: string; + moderation: string | null; + slug: string; + previous_slugs: string[] | null; + category: number | null; + tags: number[] | null; + title: string; + total_views: number | null; + tenant_id: string; + images: Json[] | null; + legacy_id: string | null; + is_draft: boolean; + }; + Insert: { + id?: number; + created_at?: string; + created_by?: number | null; + deleted?: boolean | null; + modified_at?: string; + comment_count?: number; + description: string; + moderation?: string | null; + slug: string; + previous_slugs?: string[] | null; + category?: number | null; + tags?: number[] | null; + title: string; + total_views?: number | null; + tenant_id: string; + images?: Json[] | null; + legacy_id?: string | null; + is_draft?: boolean; + }; + Update: { + id?: number; + created_at?: string; + created_by?: number | null; + deleted?: boolean | null; + modified_at?: string; + comment_count?: number; + description?: string; + moderation?: string | null; + slug?: string; + previous_slugs?: string[] | null; + category?: number | null; + tags?: number[] | null; + title?: string; + total_views?: number | null; + tenant_id?: string; + images?: Json[] | null; + legacy_id?: string | null; + is_draft?: boolean; + }; + Relationships: [ + { + foreignKeyName: 'questions_created_by_fkey'; + columns: ['created_by']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'questions_category_fkey'; + columns: ['category']; + isOneToOne: false; + referencedRelation: 'categories'; + referencedColumns: ['id']; + }, + ]; + }; + research: { + Row: { + id: number; + created_at: string; + modified_at: string | null; + title: string; + slug: string; + description: string; + category: number | null; + created_by: number | null; + tags: string[] | null; + deleted: boolean | null; + total_views: number | null; + total_useful: number | null; + previous_slugs: string[] | null; + status: Database['public']['Enums']['research_status'] | null; + is_draft: boolean | null; + tenant_id: string; + collaborators: string[] | null; + image: Json | null; + legacy_id: string | null; + }; + Insert: { + id?: number; + created_at?: string; + modified_at?: string | null; + title: string; + slug: string; + description: string; + category?: number | null; + created_by?: number | null; + tags?: string[] | null; + deleted?: boolean | null; + total_views?: number | null; + total_useful?: number | null; + previous_slugs?: string[] | null; + status?: Database['public']['Enums']['research_status'] | null; + is_draft?: boolean | null; + tenant_id: string; + collaborators?: string[] | null; + image?: Json | null; + legacy_id?: string | null; + }; + Update: { + id?: number; + created_at?: string; + modified_at?: string | null; + title?: string; + slug?: string; + description?: string; + category?: number | null; + created_by?: number | null; + tags?: string[] | null; + deleted?: boolean | null; + total_views?: number | null; + total_useful?: number | null; + previous_slugs?: string[] | null; + status?: Database['public']['Enums']['research_status'] | null; + is_draft?: boolean | null; + tenant_id?: string; + collaborators?: string[] | null; + image?: Json | null; + legacy_id?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'research_created_by_fkey'; + columns: ['created_by']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'research_category_fkey'; + columns: ['category']; + isOneToOne: false; + referencedRelation: 'categories'; + referencedColumns: ['id']; + }, + ]; + }; + research_updates: { + Row: { + id: number; + created_at: string; + research_id: number; + title: string; + description: string; + images: Json[] | null; + files: Json[] | null; + video_url: string | null; + is_draft: boolean | null; + comment_count: number | null; + tenant_id: string; + modified_at: string; + deleted: boolean | null; + file_link: string | null; + file_download_count: number | null; + created_by: number | null; + legacy_id: string | null; + }; + Insert: { + id?: number; + created_at?: string; + research_id: number; + title: string; + description: string; + images?: Json[] | null; + files?: Json[] | null; + video_url?: string | null; + is_draft?: boolean | null; + comment_count?: number | null; + tenant_id: string; + modified_at?: string; + deleted?: boolean | null; + file_link?: string | null; + file_download_count?: number | null; + created_by?: number | null; + legacy_id?: string | null; + }; + Update: { + id?: number; + created_at?: string; + research_id?: number; + title?: string; + description?: string; + images?: Json[] | null; + files?: Json[] | null; + video_url?: string | null; + is_draft?: boolean | null; + comment_count?: number | null; + tenant_id?: string; + modified_at?: string; + deleted?: boolean | null; + file_link?: string | null; + file_download_count?: number | null; + created_by?: number | null; + legacy_id?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'research_updates_research_id_fkey'; + columns: ['research_id']; + isOneToOne: false; + referencedRelation: 'research'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'research_updates_created_by_fkey'; + columns: ['created_by']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + ]; + }; + subscribers: { + Row: { + id: number; + created_at: string; + user_id: number; + content_id: number; + content_type: string; + tenant_id: string; + }; + Insert: { + id?: number; + created_at?: string; + user_id: number; + content_id: number; + content_type: string; + tenant_id: string; + }; + Update: { + id?: number; + created_at?: string; + user_id?: number; + content_id?: number; + content_type?: string; + tenant_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'subscribers_user_id_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + ]; + }; + tags: { + Row: { + id: number; + created_at: string; + name: string; + tenant_id: string; + legacy_id: string | null; + modified_at: string | null; + }; + Insert: { + id?: number; + created_at?: string; + name: string; + tenant_id: string; + legacy_id?: string | null; + modified_at?: string | null; + }; + Update: { + id?: number; + created_at?: string; + name?: string; + tenant_id?: string; + legacy_id?: string | null; + modified_at?: string | null; + }; + Relationships: []; + }; + tenant_settings: { + Row: { + id: number; + created_at: string; + site_name: string; + site_description: string | null; + site_url: string; + message_sign_off: string | null; + tenant_id: string; + email_from: string | null; + site_image: string | null; + site_favicon: string | null; + donation_settings: Json | null; + no_messaging: boolean; + library_heading: string | null; + academy_resource: string | null; + profile_guidelines: string | null; + questions_guidelines: string | null; + supported_modules: string | null; + patreon_id: string | null; + color_primary: string | null; + color_primary_hover: string | null; + color_accent: string | null; + color_accent_hover: string | null; + show_impact: boolean | null; + create_research_roles: string[] | null; + }; + Insert: { + id?: number; + created_at?: string; + site_name: string; + site_description?: string | null; + site_url: string; + message_sign_off?: string | null; + tenant_id: string; + email_from?: string | null; + site_image?: string | null; + site_favicon?: string | null; + donation_settings?: Json | null; + no_messaging?: boolean; + library_heading?: string | null; + academy_resource?: string | null; + profile_guidelines?: string | null; + questions_guidelines?: string | null; + supported_modules?: string | null; + patreon_id?: string | null; + color_primary?: string | null; + color_primary_hover?: string | null; + color_accent?: string | null; + color_accent_hover?: string | null; + show_impact?: boolean | null; + create_research_roles?: string[] | null; + }; + Update: { + id?: number; + created_at?: string; + site_name?: string; + site_description?: string | null; + site_url?: string; + message_sign_off?: string | null; + tenant_id?: string; + email_from?: string | null; + site_image?: string | null; + site_favicon?: string | null; + donation_settings?: Json | null; + no_messaging?: boolean; + library_heading?: string | null; + academy_resource?: string | null; + profile_guidelines?: string | null; + questions_guidelines?: string | null; + supported_modules?: string | null; + patreon_id?: string | null; + color_primary?: string | null; + color_primary_hover?: string | null; + color_accent?: string | null; + color_accent_hover?: string | null; + show_impact?: boolean | null; + create_research_roles?: string[] | null; + }; + Relationships: []; + }; + upgrade_badge: { + Row: { + id: number; + action_label: string; + badge_id: number; + is_space: boolean; + action_url: string; + tenant_id: string; + }; + Insert: { + id?: number; + action_label: string; + badge_id: number; + is_space: boolean; + action_url: string; + tenant_id: string; + }; + Update: { + id?: number; + action_label?: string; + badge_id?: number; + is_space?: boolean; + action_url?: string; + tenant_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'upgrade_badge_badge_id_fkey'; + columns: ['badge_id']; + isOneToOne: false; + referencedRelation: 'profile_badges'; + referencedColumns: ['id']; + }, + ]; + }; + useful_votes: { + Row: { + id: number; + created_at: string; + content_id: number; + content_type: Database['public']['Enums']['useful_content_types']; + user_id: number; + tenant_id: string; + }; + Insert: { + id?: number; + created_at?: string; + content_id: number; + content_type: Database['public']['Enums']['useful_content_types']; + user_id: number; + tenant_id: string; + }; + Update: { + id?: number; + created_at?: string; + content_id?: number; + content_type?: Database['public']['Enums']['useful_content_types']; + user_id?: number; + tenant_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'useful_votes_user_id_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + ]; + }; + }; + Views: { + [_ in never]: never; + }; + Functions: { + [_ in never]: never; + }; + Enums: { + content_types: 'questions' | 'projects' | 'research' | 'news' | 'comments'; + useful_content_types: 'questions' | 'projects' | 'research' | 'news' | 'comments'; + research_status: 'in-progress' | 'complete' | 'archived'; + notification_action_types: 'newComment' | 'newContent'; + notification_content_types: + | 'news' + | 'research' + | 'researchUpdate' + | 'library' + | 'questions' + | 'comment' + | 'reply'; + notification_source_content_type: + | 'news' + | 'research' + | 'researchUpdate' + | 'library' + | 'questions'; + }; + CompositeTypes: { + [_ in never]: never; + }; + }; +}; + +type PublicSchema = Database[Extract]; + +export type Tables< + PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & PublicSchema['Views']) | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] & + Database[PublicTableNameOrOptions['schema']]['Views']) + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? (Database[PublicTableNameOrOptions['schema']]['Tables'] & + Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends { + Row: infer R; + } + ? R + : never + : PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & PublicSchema['Views']) + ? (PublicSchema['Tables'] & PublicSchema['Views'])[PublicTableNameOrOptions] extends { + Row: infer R; + } + ? R + : never + : never; + +export type TablesInsert< + PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { + Insert: infer I; + } + ? I + : never + : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] + ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { + Insert: infer I; + } + ? I + : never + : never; + +export type TablesUpdate< + PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { + Update: infer U; + } + ? U + : never + : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] + ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { + Update: infer U; + } + ? U + : never + : never; + +export type Enums< + PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] | { schema: keyof Database }, + EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicEnumNameOrOptions['schema']]['Enums'] + : never = never, +> = PublicEnumNameOrOptions extends { schema: keyof Database } + ? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName] + : PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] + ? PublicSchema['Enums'][PublicEnumNameOrOptions] + : never; diff --git a/src/repository/supabase.server.ts b/src/repository/supabase.server.ts index fd7e875631..450774e9fd 100644 --- a/src/repository/supabase.server.ts +++ b/src/repository/supabase.server.ts @@ -1,11 +1,12 @@ import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'; +import type { Database } from 'src/database.types'; // Create a single supabase client for interacting with your database export function createSupabaseServerClient(request: Request) { const headers = new Headers(); const supabase = { headers, - client: createServerClient(process.env.SUPABASE_API_URL!, process.env.SUPABASE_KEY!, { + client: createServerClient(process.env.SUPABASE_API_URL!, process.env.SUPABASE_KEY!, { cookies: { getAll() { return parseCookieHeader(request.headers.get('Cookie') ?? '').map(({ name, value }) => ({ From 1e65ed40945823889dc7b15a947ec1054c3101b5 Mon Sep 17 00:00:00 2001 From: Georgios Metaxakis Date: Fri, 6 Mar 2026 11:35:41 +0200 Subject: [PATCH 2/4] fix: resolve type errors from typed Supabase client --- src/database.types.ts | 170 +++++++++++++++++- src/routes/api.categories.$type.ts | 2 +- .../api.discussions.$sourceId.comments.$id.ts | 6 +- ...cussions.$sourceType.$sourceId.comments.ts | 18 +- .../api.donation-settings.$profileId.ts | 14 +- src/routes/api.favicon.ts | 2 +- src/routes/api.map-filters.ts | 18 +- src/routes/api.messages.tsx | 2 +- src/routes/api.news.$id.ts | 35 +++- src/routes/api.news.ts | 11 +- src/routes/api.notifications.$id.read.ts | 2 +- src/routes/api.notifications.all.read.ts | 2 +- src/routes/api.notifications.ts | 4 +- src/routes/api.profile.ts | 4 +- src/routes/api.projects.$id.ts | 17 +- src/routes/api.projects.ts | 13 +- src/routes/api.questions.$id.ts | 61 +++++-- src/routes/api.questions.ts | 13 +- src/routes/api.research.$id.ts | 13 +- .../api.research.$id.updates.$updateId.ts | 31 ++-- src/routes/api.research.$id.updates.ts | 20 ++- src/routes/api.research.ts | 19 +- ...bers.$contentType.$contentId.subscribed.ts | 4 +- ...api.subscribers.$contentType.$contentId.ts | 6 +- .../api.useful.$contentType.$contentId.ts | 5 +- ...pi.useful.$contentType.$contentId.users.ts | 3 +- ...pi.useful.$contentType.$contentId.voted.ts | 5 +- .../api.useful.$contentType.$id.count.ts | 3 +- 28 files changed, 381 insertions(+), 122 deletions(-) diff --git a/src/database.types.ts b/src/database.types.ts index c35c5e37fd..12d7a1a4ca 100644 --- a/src/database.types.ts +++ b/src/database.types.ts @@ -1303,7 +1303,171 @@ export type Database = { [_ in never]: never; }; Functions: { - [_ in never]: never; + get_author_vote_counts: { + Args: { author_id: number }; + Returns: { content_type: string; vote_count: number }[]; + }; + get_comments_with_votes: { + Args: { + p_source_type: string; + p_source_id: number; + p_current_user_id?: number | null; + }; + Returns: { + id: number; + comment: string; + created_at: string; + modified_at: string | null; + deleted: boolean | null; + source_id: number | null; + source_type: string; + parent_id: number | null; + created_by: number | null; + profile: Json | null; + vote_count: number; + has_voted: boolean; + }[]; + }; + get_projects: { + Args: { + search_query?: string | null; + category_id?: number | null; + sort_by?: string; + limit_val?: number; + offset_val?: number; + current_username?: string | null; + days_back?: number; + }; + Returns: { + id: number; + created_at: string; + created_by: number | null; + modified_at: string | null; + description: string; + slug: string; + cover_image: Json | null; + category: Json | null; + tags: string[] | null; + title: string; + moderation: string | null; + total_views: number | null; + author: Json | null; + comment_count: number | null; + useful_votes_last_week: number | null; + }[]; + }; + get_projects_count: { + Args: { + search_query?: string | null; + category_id?: number | null; + current_username?: string | null; + }; + Returns: number; + }; + get_research: { + Args: { + search_query?: string | null; + category_id?: number | null; + research_status?: Database['public']['Enums']['research_status'] | null; + sort_by?: string; + limit_val?: number; + offset_val?: number; + days_back?: number; + }; + Returns: { + id: number; + created_at: string; + created_by: number | null; + modified_at: string | null; + description: string; + slug: string; + image: Json | null; + status: Database['public']['Enums']['research_status'] | null; + category: Json | null; + tags: string[] | null; + title: string; + total_views: number | null; + author: Json | null; + update_count: number | null; + comment_count: number | null; + useful_votes_last_week: number | null; + }[]; + }; + get_research_count: { + Args: { + search_query?: string | null; + category_id?: number | null; + research_status?: Database['public']['Enums']['research_status'] | null; + }; + Returns: number; + }; + get_subscribed_users_emails_to_notify: { + Args: { + p_content_id: number; + p_content_type: string; + }; + Returns: { + email: string; + profile_id: number; + profile_created_at: string; + display_name: string; + comments: boolean; + replies: boolean; + research_updates: boolean; + is_unsubscribed: boolean; + }[]; + }; + get_useful_votes_count_by_content_id: { + Args: { + p_content_type: Database['public']['Enums']['useful_content_types']; + p_content_ids: number[]; + }; + Returns: { content_id: number; count: number }[]; + }; + get_user_email_by_id: { + Args: { id: string }; + Returns: { email: string }[]; + }; + get_user_email_by_username: { + Args: { username: string }; + Returns: { email: string }[]; + }; + get_user_projects: { + Args: { username_param: string }; + Returns: { + id: number; + comment_count: number | null; + cover_image: Json | null; + title: string; + slug: string; + total_useful: number | null; + }[]; + }; + get_user_questions: { + Args: { username_param: string }; + Returns: { + id: number; + comment_count: number | null; + images: Json[] | null; + title: string; + slug: string; + total_useful: number | null; + }[]; + }; + get_user_research: { + Args: { username_param: string }; + Returns: { + id: number; + image: Json | null; + title: string; + slug: string; + total_useful: number | null; + }[]; + }; + is_username_available: { + Args: { username: string }; + Returns: boolean; + }; }; Enums: { content_types: 'questions' | 'projects' | 'research' | 'news' | 'comments'; @@ -1334,7 +1498,9 @@ export type Database = { type PublicSchema = Database[Extract]; export type Tables< - PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & PublicSchema['Views']) | { schema: keyof Database }, + PublicTableNameOrOptions extends + | keyof (PublicSchema['Tables'] & PublicSchema['Views']) + | { schema: keyof Database }, TableName extends PublicTableNameOrOptions extends { schema: keyof Database } ? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] & Database[PublicTableNameOrOptions['schema']]['Views']) diff --git a/src/routes/api.categories.$type.ts b/src/routes/api.categories.$type.ts index 790af53404..8480afb317 100644 --- a/src/routes/api.categories.$type.ts +++ b/src/routes/api.categories.$type.ts @@ -33,7 +33,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const { data } = await client.from('categories').select('id,name,created_at,type'); - const categories = data?.map((category) => Category.fromDB(category as DBCategory)); + const categories = data?.map((category) => Category.fromDB(category as unknown as DBCategory)); if (categories && categories.length > 0) { cache.set('categories', data, 3600000); diff --git a/src/routes/api.discussions.$sourceId.comments.$id.ts b/src/routes/api.discussions.$sourceId.comments.$id.ts index 92447e47c5..4b1035c82e 100644 --- a/src/routes/api.discussions.$sourceId.comments.$id.ts +++ b/src/routes/api.discussions.$sourceId.comments.$id.ts @@ -63,7 +63,7 @@ async function updateComment( return Response.json({}, { headers, status: 404, statusText: 'comment not found' }); } - const comment = data as DBComment; + const comment = data as unknown as DBComment; if (comment.created_by !== user.id && !isUserAdmin(user)) { return Response.json({}, { headers, status: 403, statusText: 'forbidden' }); @@ -88,7 +88,7 @@ async function deleteComment({ client, headers }: Supabase, id: string, user: DB return Response.json({}, { headers, status: 404, statusText: 'comment not found' }); } - const comment = data as DBComment; + const comment = data as unknown as DBComment; if (comment.created_by !== user.id && !isUserAdmin(user)) { return Response.json({}, { headers, status: 403, statusText: 'forbidden' }); @@ -115,7 +115,7 @@ async function getProfileByAuthId(request: Request, authId: string) { return null; } - return data[0] as DBProfile; + return data[0] as unknown as DBProfile; } function isUserAdmin(user: DBProfile) { diff --git a/src/routes/api.discussions.$sourceType.$sourceId.comments.ts b/src/routes/api.discussions.$sourceType.$sourceId.comments.ts index 0af672acbc..b1da909c6f 100644 --- a/src/routes/api.discussions.$sourceType.$sourceId.comments.ts +++ b/src/routes/api.discussions.$sourceType.$sourceId.comments.ts @@ -33,12 +33,12 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } const result = await client.rpc('get_comments_with_votes', { - p_source_type: params.sourceType, - p_source_id: params.sourceId, + p_source_type: params.sourceType!, + p_source_id: Number(params.sourceId), p_current_user_id: currentUserId || null, }); - const dbComments = result.data as DBComment[]; + const dbComments = result.data as unknown as DBComment[]; const commentFactory = new CommentFactory(new ImageServiceServer(client)); const comments = await commentFactory.fromDBCommentsToThreads(dbComments); @@ -92,16 +92,16 @@ export async function action({ params, request }: LoaderFunctionArgs) { const newComment = { comment: data.comment, source_id_legacy: isNaN(+params.sourceId!) ? params.sourceId : null, - source_id: isNaN(+params.sourceId!) ? null : +params.sourceId!, + source_id: isNaN(+params.sourceId!) ? null : Number(params.sourceId!), source_type: params.sourceType, created_by: currentUser.data[0].id, parent_id: data.parentId ?? null, tenant_id: process.env.TENANT_ID, - } as Partial; + }; const commentResult = await client .from('comments') - .insert(newComment) + .insert(newComment as any) .select( ` id, @@ -128,8 +128,8 @@ export async function action({ params, request }: LoaderFunctionArgs) { .single(); if (!commentResult.error) { - const comment = commentResult.data as DBComment; - const profile = currentUser.data[0] as DBProfile; + const comment = commentResult.data as unknown as DBComment; + const profile = currentUser.data[0] as unknown as DBProfile; addSubscriptions(comment, profile, client, headers); @@ -137,7 +137,7 @@ export async function action({ params, request }: LoaderFunctionArgs) { } const commentDb = new DBComment({ - ...(commentResult.data as DBComment), + ...(commentResult.data as unknown as DBComment), profile: (commentResult.data as any).profiles as DBAuthor, }); diff --git a/src/routes/api.donation-settings.$profileId.ts b/src/routes/api.donation-settings.$profileId.ts index 9683c2534c..4c10074045 100644 --- a/src/routes/api.donation-settings.$profileId.ts +++ b/src/routes/api.donation-settings.$profileId.ts @@ -1,3 +1,4 @@ +import type { DBMedia } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ImageServiceServer } from 'src/services/imageService.server'; @@ -8,12 +9,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { try { const donationSettingsResult = await client.from('tenant_settings').select('donation_settings'); const data = donationSettingsResult.data?.at(0); + const ds = data?.donation_settings as Record | null | undefined; const donationSettings = { spaceName: '', - campaignId: data?.donation_settings?.defaultCampaignId, - description: data?.donation_settings?.defaultDescription, - imageUrl: data?.donation_settings?.defaultImageUrl, + campaignId: ds?.defaultCampaignId, + description: ds?.defaultDescription, + imageUrl: ds?.defaultImageUrl, }; if (params.profileId) { @@ -21,17 +23,17 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const profileResult = await client .from('profiles') .select('display_name,cover_images,donations_enabled') - .eq('id', params.profileId) + .eq('id', Number(params.profileId)) .single(); if (profileResult.data?.donations_enabled) { donationSettings.spaceName = profileResult.data.display_name; donationSettings.campaignId = `${process.env.TENANT_ID}-${params.profileId}`; - donationSettings.description = data?.donation_settings?.spaceDescription; + donationSettings.description = ds?.spaceDescription; if (profileResult.data.cover_images?.at(0)) { donationSettings.imageUrl = new ImageServiceServer(client).getPublicUrl( - profileResult.data.cover_images[0], + profileResult.data.cover_images[0] as unknown as DBMedia | null, )?.publicUrl; } } diff --git a/src/routes/api.favicon.ts b/src/routes/api.favicon.ts index a6af61f173..da1e3ef962 100644 --- a/src/routes/api.favicon.ts +++ b/src/routes/api.favicon.ts @@ -27,7 +27,7 @@ export async function loader({ request }: LoaderFunctionArgs) { const { data, error } = await client .from('tenant_settings') .select('site_favicon') - .eq('tenant_id', process.env.TENANT_ID) + .eq('tenant_id', process.env.TENANT_ID!) .limit(1); if (error) { diff --git a/src/routes/api.map-filters.ts b/src/routes/api.map-filters.ts index 600fa23f19..96af371ab8 100644 --- a/src/routes/api.map-filters.ts +++ b/src/routes/api.map-filters.ts @@ -1,5 +1,13 @@ import Keyv from 'keyv'; -import type { DBMapSettings, DefaultMapFilters, FilterResponse, MapFilters } from 'oa-shared'; +import type { + DBMapSettings, + DBProfileBadge, + DBProfileTag, + DBProfileType, + DefaultMapFilters, + FilterResponse, + MapFilters, +} from 'oa-shared'; import { ProfileBadge, ProfileTag, ProfileType } from 'oa-shared'; import { isProductionEnvironment } from 'src/config/config'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; @@ -30,12 +38,12 @@ export const loader = async ({ request }) => { console.error({ message: 'Error fetching map pin filters', errors }); } - const settings = mapSettings?.data?.at(0) as DBMapSettings | undefined; + const settings = mapSettings?.data?.at(0) as unknown as DBMapSettings | undefined; const filters: MapFilters = { - tags: tags?.data?.map((x) => ProfileTag.fromDB(x)), - badges: badges?.data?.map((x) => ProfileBadge.fromDB(x)), - types: types?.data?.map((x) => ProfileType.fromDB(x)), + tags: tags?.data?.map((x) => ProfileTag.fromDB(x as unknown as DBProfileTag)), + badges: badges?.data?.map((x) => ProfileBadge.fromDB(x as unknown as DBProfileBadge)), + types: types?.data?.map((x) => ProfileType.fromDB(x as unknown as DBProfileType)), settings: settings?.setting_filters || undefined, }; diff --git a/src/routes/api.messages.tsx b/src/routes/api.messages.tsx index effeb17623..89fdd3fc01 100644 --- a/src/routes/api.messages.tsx +++ b/src/routes/api.messages.tsx @@ -89,7 +89,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { : await client.rpc('get_user_email_by_username', { username: data.to, }); - const receiver = emailResult.data[0]; + const receiver = emailResult.data![0]; const messenger = userProfile.data![0]; const emailTemplate = ( diff --git a/src/routes/api.news.$id.ts b/src/routes/api.news.$id.ts index f0705669a4..02be177fc7 100644 --- a/src/routes/api.news.$id.ts +++ b/src/routes/api.news.$id.ts @@ -3,6 +3,7 @@ import type { DBNews } from 'oa-shared'; import { News } from 'oa-shared'; import type { LoaderFunctionArgs, Params } from 'react-router'; import { IMAGE_SIZES } from 'src/config/imageTransforms'; +import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { contentServiceServer } from 'src/services/contentService.server'; import { newsServiceServer } from 'src/services/newsService.server'; @@ -38,7 +39,14 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { const currentNews = await newsServiceServer.getById(id, client); - const { valid, status, statusText } = await validateRequest(params, request, claims.data.claims.sub, data, currentNews, client); + const { valid, status, statusText } = await validateRequest( + params, + request, + claims.data.claims.sub, + data, + currentNews, + client, + ); if (!valid) { return Response.json({}, { headers, status, statusText }); @@ -67,10 +75,10 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { body: data.body, category: data.category, is_draft: data.isDraft, - modified_at: new Date(), + modified_at: new Date().toISOString(), slug: data.slug, previous_slugs: previousSlugs, - profile_badge: data.profileBadge, + profile_badge: data.profileBadge ? Number(data.profileBadge) : null, summary: getSummaryFromMarkdown(data.body), tags: data.tags, title: data.title, @@ -83,20 +91,28 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { throw newsResult.error; } - const news = News.fromDB(newsResult.data[0], []); + const news = News.fromDB(newsResult.data[0] as unknown as DBNews, []); if (newHeroImage) { - const mediaFiles = await storageServiceServer.uploadImage([newHeroImage], `news/${news.id}`, client); + const mediaFiles = await storageServiceServer.uploadImage( + [newHeroImage], + `news/${news.id}`, + client, + ); if (mediaFiles?.media?.length) { await client .from('news') .update({ - hero_image: mediaFiles.media.at(0), + hero_image: mediaFiles.media.at(0) as unknown as Json, }) .eq('id', news.id); - const [image] = storageServiceServer.getPublicUrls(client, mediaFiles.media, IMAGE_SIZES.GALLERY); + const [image] = storageServiceServer.getPublicUrls( + client, + mediaFiles.media, + IMAGE_SIZES.GALLERY, + ); news.heroImage = image; } @@ -139,7 +155,10 @@ async function validateRequest( return { status: 400, statusText: 'News not found' }; } - if (currentNews.slug !== data.slug && (await contentServiceServer.isDuplicateExistingSlug(data.slug, currentNews.id, client, 'news'))) { + if ( + currentNews.slug !== data.slug && + (await contentServiceServer.isDuplicateExistingSlug(data.slug, currentNews.id, client, 'news')) + ) { return { status: 409, statusText: 'This news already exists', diff --git a/src/routes/api.news.ts b/src/routes/api.news.ts index 4b306b04d8..7529f94738 100644 --- a/src/routes/api.news.ts +++ b/src/routes/api.news.ts @@ -4,6 +4,7 @@ import type { AuthError } from '@supabase/supabase-js'; import type { DBNews, DBProfile, Moderation } from 'oa-shared'; import { News } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; +import type { Json } from 'src/database.types'; import { ITEMS_PER_PAGE } from 'src/pages/News/constants'; import type { NewsSortOption } from 'src/pages/News/NewsSortOptions'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; @@ -190,15 +191,15 @@ export const action = async ({ request }: LoaderFunctionArgs) => { .from('news') .insert({ body: data.body, - category: data.category, + category: data.category ? Number(data.category) : null, created_by: profile.id, is_draft: data.isDraft, moderation: 'accepted' as Moderation, - profile_badge: data.profileBadge, + profile_badge: data.profileBadge ? Number(data.profileBadge) : null, slug, summary: getSummaryFromMarkdown(data.body), tags: data.tags, - tenant_id: process.env.TENANT_ID, + tenant_id: process.env.TENANT_ID!, title: data.title, }) .select(); @@ -207,7 +208,7 @@ export const action = async ({ request }: LoaderFunctionArgs) => { throw newsResult.error; } - const news = News.fromDB(newsResult.data[0], []); + const news = News.fromDB(newsResult.data[0] as unknown as DBNews, []); subscribersServiceServer.add('news', news.id, profile.id, client, headers); if (!news.isDraft) { @@ -225,7 +226,7 @@ export const action = async ({ request }: LoaderFunctionArgs) => { await client .from('news') .update({ - hero_image: mediaFiles.media.at(0), + hero_image: mediaFiles.media.at(0) as unknown as Json, }) .eq('id', news.id); diff --git a/src/routes/api.notifications.$id.read.ts b/src/routes/api.notifications.$id.read.ts index 0ffd0a7453..453b611b74 100644 --- a/src/routes/api.notifications.$id.read.ts +++ b/src/routes/api.notifications.$id.read.ts @@ -18,7 +18,7 @@ export const action = async ({ params, request }: LoaderFunctionArgs) => { return Response.json({}, { headers, status, statusText }); } - await client.from('notifications').update({ is_read: true }).eq('id', params.id); + await client.from('notifications').update({ is_read: true }).eq('id', Number(params.id)); updateUserActivity(client, claims.data.claims.sub); diff --git a/src/routes/api.notifications.all.read.ts b/src/routes/api.notifications.all.read.ts index ffd89589cc..b5fef56a76 100644 --- a/src/routes/api.notifications.all.read.ts +++ b/src/routes/api.notifications.all.read.ts @@ -27,7 +27,7 @@ export const action = async ({ request }: LoaderFunctionArgs) => { await client .from('notifications') .update({ is_read: true }) - .eq('owned_by_id', profile?.data?.id); + .eq('owned_by_id', profile.data!.id); updateUserActivity(client, claims.data.claims.sub); diff --git a/src/routes/api.notifications.ts b/src/routes/api.notifications.ts index 68587ebb0a..e857adb0ce 100644 --- a/src/routes/api.notifications.ts +++ b/src/routes/api.notifications.ts @@ -75,7 +75,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { throw error; } - const notifications = data?.length ? await transformNotificationList(data, client) : []; + const notifications = data?.length + ? await transformNotificationList(data as unknown as DBNotification[], client) + : []; return Response.json({ notifications }, { headers, status: 200 }); } catch (error) { diff --git a/src/routes/api.profile.ts b/src/routes/api.profile.ts index 4926008638..4921cf9b86 100644 --- a/src/routes/api.profile.ts +++ b/src/routes/api.profile.ts @@ -1,4 +1,4 @@ -import type { Image, ProfileFormData } from 'oa-shared'; +import type { DBProfile, Image, ProfileFormData } from 'oa-shared'; import type { ActionFunctionArgs } from 'react-router'; import { ProfileFactory } from 'src/factories/profileFactory.server'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; @@ -58,7 +58,7 @@ export const loader = async ({ request }) => { } const profileFactory = new ProfileFactory(client); - const profile = profileFactory.fromDB(data); + const profile = profileFactory.fromDB(data as unknown as DBProfile); return Response.json(profile, { headers, status: 200 }); } catch (error) { diff --git a/src/routes/api.projects.$id.ts b/src/routes/api.projects.$id.ts index 674bdd595c..2f5bb43823 100644 --- a/src/routes/api.projects.$id.ts +++ b/src/routes/api.projects.$id.ts @@ -1,7 +1,8 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBMedia, DBProfile, DBProject, MediaFile } from 'oa-shared'; +import type { DBMedia, DBProfile, DBProject, Image, MediaFile } from 'oa-shared'; import { Project, ProjectStep, UserRole } from 'oa-shared'; import type { ActionFunctionArgs } from 'react-router'; +import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { contentServiceServer } from 'src/services/contentService.server'; import { libraryServiceServer } from 'src/services/libraryService.server'; @@ -137,11 +138,11 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const { data } = await client .from('project_steps') - .update({ images }) + .update({ images: images as unknown as Json[] }) .eq('id', stepDb.id) .select('images') .single(); - step.images = data?.images; + step.images = data?.images as unknown as Image[]; project.steps.push(step); } @@ -253,9 +254,9 @@ async function updateProject( file_link: data.fileLink, difficulty_level: data.difficultyLevel, time: data.time, - files, + files: files as unknown as Json[], moderation, - cover_image, + cover_image: cover_image as unknown as Json, }) .eq('id', currentProject.id) .select(); @@ -284,7 +285,9 @@ async function updateOrReplaceImages( .single(); if (existingMedia.data && existingMedia.data.images?.length > 0) { - media = existingMedia.data.images.filter((x) => idsToKeep.includes(x.id)); + media = (existingMedia.data.images as unknown as DBMedia[]).filter((x) => + idsToKeep.includes(x.id), + ); } // TODO: delete other images } @@ -318,7 +321,7 @@ async function deleteProject(request: Request, id: number) { await client .from('projects') .update({ - modified_at: new Date(), + modified_at: new Date().toISOString(), deleted: true, }) .eq('id', id); diff --git a/src/routes/api.projects.ts b/src/routes/api.projects.ts index 4bbcb6fbaf..26013555d2 100644 --- a/src/routes/api.projects.ts +++ b/src/routes/api.projects.ts @@ -1,8 +1,9 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBProfile, DBProject, DBProjectStep, Moderation } from 'oa-shared'; +import type { DBProfile, DBProject, DBProjectStep, MediaFile, Moderation } from 'oa-shared'; import { Project, ProjectStep, UserRole } from 'oa-shared'; import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'; import { IMAGE_SIZES } from 'src/config/imageTransforms'; +import type { Json } from 'src/database.types'; import type { LibrarySortOption } from 'src/pages/Library/Content/List/LibrarySortOptions'; import { ITEMS_PER_PAGE } from 'src/pages/Library/constants'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; @@ -48,7 +49,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { return Response.json({}, { status: 500, headers }); } - const dbItems = data as DBProject[]; + const dbItems = data as unknown as DBProject[]; const items = dbItems.map((x) => { const images = x.cover_image ? storageServiceServer.getPublicUrls(client, [x.cover_image], IMAGE_SIZES.LIST) @@ -255,7 +256,7 @@ async function createProject( difficulty_level: data.difficultyLevel, time: data.time, moderation: data.moderation, - tenant_id: process.env.TENANT_ID, + tenant_id: process.env.TENANT_ID!, }) .select(); @@ -284,7 +285,7 @@ async function createStep( project_id: values.projectId, video_url: values.videoUrl, order: values.order, - tenant_id: process.env.TENANT_ID, + tenant_id: process.env.TENANT_ID!, }) .select(); @@ -332,13 +333,13 @@ async function uploadAndUpdateFiles( const result = await client .from('projects') .update({ - files: mediaResult.media, + files: mediaResult.media as unknown as Json[], }) .eq('id', project.id) .select(); if (result.data) { - project.files = result.data[0].files; + project.files = result.data[0].files as unknown as MediaFile[]; } } } diff --git a/src/routes/api.questions.$id.ts b/src/routes/api.questions.$id.ts index b3413728ee..c4e18ee588 100644 --- a/src/routes/api.questions.$id.ts +++ b/src/routes/api.questions.$id.ts @@ -3,6 +3,7 @@ import type { DBMedia, DBQuestion } from 'oa-shared'; import { Question } from 'oa-shared'; import type { LoaderFunctionArgs, Params } from 'react-router'; import { IMAGE_SIZES } from 'src/config/imageTransforms'; +import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; import { questionServiceServer } from 'src/services/questionService.server'; @@ -39,7 +40,14 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { const currentQuestion = await questionServiceServer.getById(id, client); - const { valid, status, statusText } = await validateRequest(params, request, claims.data.claims.sub, data, currentQuestion, client); + const { valid, status, statusText } = await validateRequest( + params, + request, + claims.data.claims.sub, + data, + currentQuestion, + client, + ); if (!valid) { return Response.json({}, { headers, status, statusText }); @@ -62,15 +70,29 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { let images: DBMedia[] = []; if (imagesToKeepIds.length > 0) { - const questionImages = await client.from('questions').select('images').eq('id', params.id).single(); - - if (questionImages.data && questionImages.data?.images?.length > 0) { - images = questionImages.data.images.filter((x) => imagesToKeepIds.includes(x.id)); + const questionImages = await client + .from('questions') + .select('images') + .eq('id', Number(params.id)) + .single(); + + if ( + questionImages.data && + questionImages.data.images && + questionImages.data.images.length > 0 + ) { + images = (questionImages.data.images as unknown as DBMedia[]).filter((x) => + imagesToKeepIds.includes(x.id), + ); } } if (uploadedImages.length > 0) { - const mediaResult = await storageServiceServer.uploadImage(uploadedImages, `questions/${id}`, client); + const mediaResult = await storageServiceServer.uploadImage( + uploadedImages, + `questions/${id}`, + client, + ); if (mediaResult) { images = [...images, ...mediaResult.media]; @@ -85,23 +107,31 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { category: data.category, description: data.description, is_draft: data.is_draft, - images, + images: images as unknown as Json[], title: data.title, slug: data.slug, previous_slugs: previousSlugs, tags: data.tags, - modified_at: new Date(), + modified_at: new Date().toISOString(), }) - .eq('id', params.id) + .eq('id', Number(params.id)) .select(); if (questionResult.error || !questionResult.data) { throw questionResult.error; } - const newImages = storageServiceServer.getPublicUrls(client, questionResult.data[0].images, IMAGE_SIZES.GALLERY); - - const question = Question.fromDB(questionResult.data[0], [], newImages); + const newImages = storageServiceServer.getPublicUrls( + client, + questionResult.data[0].images as unknown as DBMedia[], + IMAGE_SIZES.GALLERY, + ); + + const question = Question.fromDB( + questionResult.data[0] as unknown as DBQuestion, + [], + newImages, + ); updateUserActivity(client, claims.data.claims.sub); return Response.json({ question }, { headers, status: 200 }); @@ -141,7 +171,12 @@ async function validateRequest( if ( currentQuestion.slug !== data.slug && - (await contentServiceServer.isDuplicateExistingSlug(data.slug, currentQuestion.id, client, 'questions')) + (await contentServiceServer.isDuplicateExistingSlug( + data.slug, + currentQuestion.id, + client, + 'questions', + )) ) { return { status: 409, diff --git a/src/routes/api.questions.ts b/src/routes/api.questions.ts index 14df11cda5..9b58784cf8 100644 --- a/src/routes/api.questions.ts +++ b/src/routes/api.questions.ts @@ -1,8 +1,9 @@ // TODO: split this in separate files once we update remix to NOT use file-based routing -import type { DBProfile, DBQuestion, Moderation } from 'oa-shared'; +import type { DBProfile, DBQuestion, Image, Moderation } from 'oa-shared'; import { Question } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; +import type { Json } from 'src/database.types'; import { ITEMS_PER_PAGE } from 'src/pages/Question/constants'; import type { QuestionSortOption } from 'src/pages/Question/QuestionSortOptions'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; @@ -173,9 +174,9 @@ export const action = async ({ request }: LoaderFunctionArgs) => { is_draft: data.is_draft, moderation: 'accepted' as Moderation, slug, - category: data.category, + category: data.category ? Number(data.category) : null, tags: data.tags, - tenant_id: process.env.TENANT_ID, + tenant_id: process.env.TENANT_ID!, }) .select(); @@ -183,7 +184,7 @@ export const action = async ({ request }: LoaderFunctionArgs) => { return Response.json({}, { headers, status: 400, statusText: questionResult.error.details }); } - const question = Question.fromDB(questionResult.data[0], []); + const question = Question.fromDB(questionResult.data[0] as unknown as DBQuestion, []); subscribersServiceServer.add('questions', question.id, profile.id, client, headers); if (uploadedImages.length > 0) { @@ -198,12 +199,12 @@ export const action = async ({ request }: LoaderFunctionArgs) => { if (imageResult?.media && imageResult.media.length > 0) { const updateResult = await client .from('questions') - .update({ images: imageResult.media }) + .update({ images: imageResult.media as unknown as Json[] }) .eq('id', questionId) .select(); if (updateResult.data) { - question.images = updateResult.data[0].images; + question.images = updateResult.data[0].images as unknown as Image[]; } } } diff --git a/src/routes/api.research.$id.ts b/src/routes/api.research.$id.ts index 565aa72304..436e54d257 100644 --- a/src/routes/api.research.$id.ts +++ b/src/routes/api.research.$id.ts @@ -1,7 +1,8 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBResearchItem } from 'oa-shared'; +import type { DBMedia, DBResearchItem } from 'oa-shared'; import { ResearchItem, UserRole } from 'oa-shared'; import type { ActionFunctionArgs } from 'react-router'; +import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { contentServiceServer } from 'src/services/contentService.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; @@ -65,7 +66,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { description: data.description, slug: data.slug, category: data.category, - tags: data.tags, + tags: data.tags as unknown as string[], previous_slugs: previousSlugs, is_draft: data.isDraft, collaborators: data.collaborators, @@ -79,7 +80,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { throw researchResult.error; } - const research = ResearchItem.fromDB(researchResult.data, []); + const research = ResearchItem.fromDB(researchResult.data as unknown as DBResearchItem, []); await subscribersServiceServer.updateResearchSubscribers( oldResearch, @@ -103,14 +104,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { if (mediaResult?.media && mediaResult.media.length > 0) { const result = await client .from('research') - .update({ image: mediaResult.media[0] }) + .update({ image: mediaResult.media[0] as unknown as Json }) .eq('id', research.id) .select('image'); if (result.data && result.data.length > 0) { const [image] = storageServiceServer.getPublicUrls( client, - result.data?.at(0)?.image ? [result.data[0].image] : [], + result.data?.at(0)?.image ? [result.data[0].image as unknown as DBMedia] : [], ); research.image = image; @@ -147,7 +148,7 @@ async function deleteResearch(request, id: number) { await client .from('research') .update({ - modified_at: new Date(), + modified_at: new Date().toISOString(), deleted: true, }) .eq('id', id); diff --git a/src/routes/api.research.$id.updates.$updateId.ts b/src/routes/api.research.$id.updates.$updateId.ts index 367b5f8d74..c25a752314 100644 --- a/src/routes/api.research.$id.updates.$updateId.ts +++ b/src/routes/api.research.$id.updates.$updateId.ts @@ -1,7 +1,8 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBMedia, DBResearchUpdate, MediaFile } from 'oa-shared'; +import type { DBMedia, DBResearchItem, DBResearchUpdate, MediaFile } from 'oa-shared'; import { ResearchUpdate } from 'oa-shared'; import type { ActionFunctionArgs } from 'react-router'; +import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { broadcastCoordinationServiceServer } from 'src/services/broadcastCoordinationService.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; @@ -58,7 +59,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { .eq('id', updateId) .single(); - const oldResearchUpdate = researchUpdateResult.data as DBResearchUpdate; + const oldResearchUpdate = researchUpdateResult.data as unknown as DBResearchUpdate; const uploadedImages = formData.getAll('images') as File[]; const uploadedFiles = formData.getAll('files') as File[]; @@ -98,8 +99,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { title: data.title, description: data.description, is_draft: data.isDraft, - images, - modified_at: new Date(), + images: images as unknown as Json[], + modified_at: new Date().toISOString(), video_url: data.videoUrl, files: files.map((x) => ({ id: x.id, name: x.name, size: x.size })), }) @@ -111,8 +112,12 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { throw researchUpdateAfterUpdating.error; } - const researchUpdate = ResearchUpdate.fromDB(researchUpdateAfterUpdating.data, []); - researchUpdate.research = researchUpdateAfterUpdating.data.research; + const researchUpdate = ResearchUpdate.fromDB( + researchUpdateAfterUpdating.data as unknown as DBResearchUpdate, + [], + ); + researchUpdate.research = researchUpdateAfterUpdating.data + .research as unknown as DBResearchItem; broadcastCoordinationServiceServer.researchUpdate( researchUpdate, @@ -149,7 +154,9 @@ async function updateOrReplaceImage( .single(); if (existingMedia.data && existingMedia.data.images?.length > 0) { - media = existingMedia.data.images.filter((x) => idsToKeep.includes(x.id)); + media = (existingMedia.data.images as unknown as DBMedia[]).filter((x) => + idsToKeep.includes(x.id), + ); } } @@ -182,8 +189,12 @@ async function updateOrReplaceFile( let mediaToRemove: MediaFile[] = []; if (existingMedia.data && existingMedia.data.files?.length > 0) { - media = existingMedia.data.files.filter((x) => idsToKeep.includes(x.id)); - mediaToRemove = existingMedia.data.files.filter((x) => !idsToKeep.includes(x.id)); + media = (existingMedia.data.files as unknown as MediaFile[]).filter((x) => + idsToKeep.includes(x.id), + ); + mediaToRemove = (existingMedia.data.files as unknown as MediaFile[]).filter( + (x) => !idsToKeep.includes(x.id), + ); } if (mediaToRemove.length > 0) { @@ -223,7 +234,7 @@ async function deleteResearchUpdate(request, id: number, updateId: number) { await client .from('research_updates') .update({ - modified_at: new Date(), + modified_at: new Date().toISOString(), deleted: true, }) .eq('id', updateId); diff --git a/src/routes/api.research.$id.updates.ts b/src/routes/api.research.$id.updates.ts index 0629c19a52..80b81313aa 100644 --- a/src/routes/api.research.$id.updates.ts +++ b/src/routes/api.research.$id.updates.ts @@ -1,7 +1,8 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBProfile, DBResearchItem } from 'oa-shared'; +import type { DBProfile, DBResearchItem, DBResearchUpdate, Image, MediaFile } from 'oa-shared'; import { ResearchUpdate, UserRole } from 'oa-shared'; import type { ActionFunctionArgs } from 'react-router'; +import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { broadcastCoordinationServiceServer } from 'src/services/broadcastCoordinationService.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; @@ -73,7 +74,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { is_draft: data.isDraft, research_id: researchId, created_by: profile.id, - tenant_id: process.env.TENANT_ID, + tenant_id: process.env.TENANT_ID!, }) .select('*,research:research(id,title,collaborators,created_by,is_draft,slug)') .single(); @@ -83,8 +84,11 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { } const dbResearchUpdate = updateResult.data; - const researchUpdate = ResearchUpdate.fromDB(dbResearchUpdate, []); - researchUpdate.research = updateResult.data.research; + const researchUpdate = ResearchUpdate.fromDB( + dbResearchUpdate as unknown as DBResearchUpdate, + [], + ); + researchUpdate.research = updateResult.data.research as unknown as DBResearchItem; await uploadAndUpdateImages( uploadedImages, @@ -137,13 +141,13 @@ async function uploadAndUpdateImages( const result = await client .from('research_updates') .update({ - images: mediaResult.media, + images: mediaResult.media as unknown as Json[], }) .eq('id', researchUpdate.id) .select(); if (result.data) { - researchUpdate.images = result.data[0].images; + researchUpdate.images = result.data[0].images as unknown as Image[]; } } } @@ -162,13 +166,13 @@ async function uploadAndUpdateFiles( const result = await client .from('research_updates') .update({ - files: mediaResult.media, + files: mediaResult.media as unknown as Json[], }) .eq('id', researchUpdate.id) .select(); if (result.data) { - researchUpdate.files = result.data[0].files; + researchUpdate.files = result.data[0].files as unknown as MediaFile[]; } } } diff --git a/src/routes/api.research.ts b/src/routes/api.research.ts index e0bf48a810..464657d068 100644 --- a/src/routes/api.research.ts +++ b/src/routes/api.research.ts @@ -1,7 +1,8 @@ -import type { DBResearchItem, ResearchStatus } from 'oa-shared'; +import type { DBResearchItem, Image, ResearchStatus } from 'oa-shared'; import { ResearchItem } from 'oa-shared'; import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'; import { IMAGE_SIZES } from 'src/config/imageTransforms'; +import type { Json } from 'src/database.types'; import { ITEMS_PER_PAGE } from 'src/pages/Research/constants'; import type { ResearchSortOption } from 'src/pages/Research/ResearchSortOptions.ts'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; @@ -44,7 +45,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { return Response.json({}, { status: 500, headers }); } - const dbItems = data as DBResearchItem[]; + const dbItems = data as unknown as DBResearchItem[]; if (!dbItems || dbItems.length === 0) { return Response.json({ items: [], total: 0 }, { headers }); @@ -137,12 +138,12 @@ export const action = async ({ request }: ActionFunctionArgs) => { title: data.title, description: data.description, slug, - category: data.category, - tags: data.tags, + category: data.category ? Number(data.category) : null, + tags: data.tags as unknown as string[], collaborators: data.collaborators, status: researchStatus, is_draft: data.isDraft, - tenant_id: process.env.TENANT_ID, + tenant_id: process.env.TENANT_ID!, }) .select() .single(); @@ -152,10 +153,10 @@ export const action = async ({ request }: ActionFunctionArgs) => { } const research = ResearchItem.fromDB( - researchResult.data, + researchResult.data as unknown as DBResearchItem, + [], [], [], - researchResult.data.collaborators, ); await subscribersServiceServer.addResearchSubscribers(research, profile.id, client, headers); @@ -174,12 +175,12 @@ export const action = async ({ request }: ActionFunctionArgs) => { if (mediaResult?.media && mediaResult.media.length > 0) { const result = await client .from('research') - .update({ image: mediaResult.media[0] }) + .update({ image: mediaResult.media[0] as unknown as Json }) .eq('id', research.id) .select(); if (result.data) { - research.image = result.data[0].image; + research.image = result.data[0].image as unknown as Image | null; } } } diff --git a/src/routes/api.subscribers.$contentType.$contentId.subscribed.ts b/src/routes/api.subscribers.$contentType.$contentId.subscribed.ts index ce0d3f9298..4f4f69ed4a 100644 --- a/src/routes/api.subscribers.$contentType.$contentId.subscribed.ts +++ b/src/routes/api.subscribers.$contentType.$contentId.subscribed.ts @@ -12,8 +12,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const { data } = await client .from('subscribers') .select('id, profiles!inner(id)') - .eq('content_id', params.contentId) - .eq('content_type', params.contentType) + .eq('content_id', Number(params.contentId)) + .eq('content_type', params.contentType!) .eq('profiles.auth_id', claims.data.claims.sub); const subscribed = !!data && !(data.length === 0); diff --git a/src/routes/api.subscribers.$contentType.$contentId.ts b/src/routes/api.subscribers.$contentType.$contentId.ts index 88c31e6588..614ecd9d1d 100644 --- a/src/routes/api.subscribers.$contentType.$contentId.ts +++ b/src/routes/api.subscribers.$contentType.$contentId.ts @@ -26,7 +26,7 @@ export async function action({ request, params }: LoaderFunctionArgs) { const response = await client .from('subscribers') .select('*') - .eq('content_type', params.contentType) + .eq('content_type', params.contentType!) .eq('content_id', Number(params.contentId)) .eq('user_id', profile.id) .single(); @@ -36,7 +36,7 @@ export async function action({ request, params }: LoaderFunctionArgs) { } await client.from('subscribers').insert({ - content_type: params.contentType, + content_type: params.contentType!, content_id: Number(params.contentId), user_id: profile.id, tenant_id: process.env.TENANT_ID!, @@ -47,7 +47,7 @@ export async function action({ request, params }: LoaderFunctionArgs) { await client .from('subscribers') .delete() - .eq('content_type', params.contentType) + .eq('content_type', params.contentType!) .eq('content_id', Number(params.contentId)) .eq('user_id', profile.id); } diff --git a/src/routes/api.useful.$contentType.$contentId.ts b/src/routes/api.useful.$contentType.$contentId.ts index 60453d2c67..a0200391ee 100644 --- a/src/routes/api.useful.$contentType.$contentId.ts +++ b/src/routes/api.useful.$contentType.$contentId.ts @@ -1,4 +1,5 @@ import type { LoaderFunctionArgs } from 'react-router'; +import type { Database } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { updateUserActivity } from 'src/utils/activity.server'; @@ -29,7 +30,7 @@ export async function action({ request, params }: LoaderFunctionArgs) { let result; if (request.method === 'POST') { result = await client.from('useful_votes').insert({ - content_type: params.contentType, + content_type: params.contentType as Database['public']['Enums']['useful_content_types'], content_id: Number(params.contentId), user_id: profileResult.data[0].id, tenant_id: process.env.TENANT_ID!, @@ -38,7 +39,7 @@ export async function action({ request, params }: LoaderFunctionArgs) { result = await client .from('useful_votes') .delete() - .eq('content_type', params.contentType) + .eq('content_type', params.contentType as Database['public']['Enums']['useful_content_types']) .eq('content_id', Number(params.contentId)) .eq('user_id', profileResult.data[0].id) .eq('tenant_id', process.env.TENANT_ID!); diff --git a/src/routes/api.useful.$contentType.$contentId.users.ts b/src/routes/api.useful.$contentType.$contentId.users.ts index 894794409b..22f44b561c 100644 --- a/src/routes/api.useful.$contentType.$contentId.users.ts +++ b/src/routes/api.useful.$contentType.$contentId.users.ts @@ -1,5 +1,6 @@ import type { DBProfile, ProfileListItem } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; +import type { Database } from 'src/database.types'; import { ProfileFactory } from 'src/factories/profileFactory.server'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; @@ -33,7 +34,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { action_url )))`, ) - .eq('content_type', contentType) + .eq('content_type', contentType as Database['public']['Enums']['useful_content_types']) .eq('content_id', Number(contentId)) .eq('tenant_id', process.env.TENANT_ID!); diff --git a/src/routes/api.useful.$contentType.$contentId.voted.ts b/src/routes/api.useful.$contentType.$contentId.voted.ts index a75fe43706..30f152ec55 100644 --- a/src/routes/api.useful.$contentType.$contentId.voted.ts +++ b/src/routes/api.useful.$contentType.$contentId.voted.ts @@ -1,4 +1,5 @@ import type { LoaderFunctionArgs } from 'react-router'; +import type { Database } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; export async function loader({ request, params }: LoaderFunctionArgs) { @@ -13,8 +14,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const useful = await client .from('useful_votes') .select('id, profiles!inner(id)', { count: 'exact' }) - .eq('content_id', params.contentId) - .eq('content_type', params.contentType) + .eq('content_id', Number(params.contentId)) + .eq('content_type', params.contentType as Database['public']['Enums']['useful_content_types']) .eq('profiles.auth_id', claims.data.claims.sub); const voted = useful.count === 1; diff --git a/src/routes/api.useful.$contentType.$id.count.ts b/src/routes/api.useful.$contentType.$id.count.ts index f61c5001aa..4f882422e4 100644 --- a/src/routes/api.useful.$contentType.$id.count.ts +++ b/src/routes/api.useful.$contentType.$id.count.ts @@ -1,5 +1,6 @@ import type { UsefulContentType } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; +import type { Database } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; export async function loader({ request, params }: LoaderFunctionArgs) { @@ -28,7 +29,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const { data, error } = await client .from('useful_votes') .select('content_id') - .eq('content_type', contentType) + .eq('content_type', contentType as Database['public']['Enums']['useful_content_types']) .eq('content_id', commentId); if (error) { From 775dd05e68ae8330e1ce1f611c78b632c2d6fe40 Mon Sep 17 00:00:00 2001 From: Georgios Metaxakis Date: Fri, 6 Mar 2026 13:28:17 +0200 Subject: [PATCH 3/4] refactor: replace as unknown as casts with semantic type helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Align DB model date types with Supabase returns (Date → string) - Create supabase.types.ts with toJson, fromJson, fromJsonArray, dbResult, dbResultArray helpers - Replace all 56 as unknown as casts in route files with helpers - Update IDBDocSB base interface and all implementing types - Fix mock data and test factories for string date types --- shared/mocks/data/news.ts | 6 +- shared/mocks/data/questions.ts | 8 +-- shared/mocks/data/research.ts | 60 +++++++++---------- shared/mocks/data/researchUpdates.ts | 16 ++--- shared/mocks/data/tags.ts | 20 +++---- shared/models/banner.ts | 4 +- shared/models/category.ts | 4 +- shared/models/comment.ts | 4 +- shared/models/document.ts | 4 +- shared/models/library.ts | 4 +- shared/models/news.ts | 4 +- shared/models/profile.ts | 12 ++-- shared/models/profileTag.ts | 2 +- shared/models/question.ts | 4 +- shared/models/research.ts | 8 +-- shared/models/tag.ts | 4 +- src/routes/_.library.$slug._index.tsx | 3 +- src/routes/_.library.$slug.edit.tsx | 6 +- src/routes/_.news.$slug._index.tsx | 3 +- src/routes/_.news.$slug.edit.tsx | 3 +- src/routes/_.questions.$slug._index.tsx | 3 +- src/routes/_.questions.$slug.edit.tsx | 3 +- src/routes/_.research.$slug.edit.tsx | 3 +- src/routes/_.research.$slug.new-update.tsx | 3 +- src/routes/api.categories.$type.ts | 3 +- .../api.discussions.$sourceId.comments.$id.ts | 7 ++- ...cussions.$sourceType.$sourceId.comments.ts | 9 +-- .../api.donation-settings.$profileId.ts | 3 +- src/routes/api.map-filters.ts | 9 +-- src/routes/api.map-pin.ts | 3 +- src/routes/api.map-pins.$userId.ts | 3 +- src/routes/api.news.$id.ts | 6 +- src/routes/api.news.drafts.ts | 3 +- src/routes/api.news.ts | 8 +-- src/routes/api.notifications.ts | 3 +- src/routes/api.profile-tags.ts | 3 +- src/routes/api.profile.ts | 3 +- src/routes/api.profiles.ts | 3 +- src/routes/api.projects.$id.ts | 13 ++-- src/routes/api.projects.drafts.ts | 3 +- src/routes/api.projects.ts | 11 ++-- src/routes/api.questions.$id.ts | 9 +-- src/routes/api.questions.drafts.ts | 3 +- src/routes/api.questions.ts | 9 +-- src/routes/api.research.$id.ts | 10 ++-- .../api.research.$id.updates.$updateId.ts | 18 +++--- src/routes/api.research.$id.updates.ts | 15 ++--- src/routes/api.research.drafts.ts | 3 +- src/routes/api.research.ts | 12 ++-- src/routes/api.settings.impact.ts | 3 +- src/routes/api.settings.map.ts | 3 +- ...pi.useful.$contentType.$contentId.users.ts | 3 +- src/test/factories/ResearchItem.ts | 8 +-- src/test/factories/dbNotification.ts | 2 +- src/test/factories/profile.ts | 4 +- src/utils/supabase.types.ts | 23 +++++++ 56 files changed, 233 insertions(+), 176 deletions(-) create mode 100644 src/utils/supabase.types.ts diff --git a/shared/mocks/data/news.ts b/shared/mocks/data/news.ts index e831dd3a77..4e85dd4b57 100644 --- a/shared/mocks/data/news.ts +++ b/shared/mocks/data/news.ts @@ -5,7 +5,7 @@ export const news: Partial[] = [ body: 'Test info with a link to [OneArmy](https://www.onearmy.earth/).\n![test-img](https://wbskztclbriekwpehznv.supabase.co/storage/v1/object/public/precious-plastic/pp-logo.png)', category: null, comment_count: 2, - created_at: new Date(), + created_at: new Date().toISOString(), deleted: false, // hero_image: { // id: '30', @@ -23,7 +23,7 @@ export const news: Partial[] = [ body: 'This is a test mock for the filtering question.', category: null, comment_count: 2, - created_at: new Date(), + created_at: new Date().toISOString(), deleted: false, // hero_image: { // id: '31', @@ -41,7 +41,7 @@ export const news: Partial[] = [ body: "What's the deal with screenings?", category: null, comment_count: 2, - created_at: new Date(), + created_at: new Date().toISOString(), deleted: false, // hero_image: { // id: '32', diff --git a/shared/mocks/data/questions.ts b/shared/mocks/data/questions.ts index 137dd96dd3..5650832c22 100644 --- a/shared/mocks/data/questions.ts +++ b/shared/mocks/data/questions.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker'; export const questions = [ { - created_at: new Date().toUTCString(), + created_at: new Date().toISOString(), deleted: false, comment_count: 3, description: 'test info with a link to https://www.onearmy.earth/', @@ -11,7 +11,7 @@ export const questions = [ total_views: 3, }, { - created_at: new Date().toUTCString(), + created_at: new Date().toISOString(), created_by: 'demo_user', deleted: false, comment_count: 3, @@ -21,7 +21,7 @@ export const questions = [ total_views: 43, }, { - created_at: new Date().toUTCString(), + created_at: new Date().toISOString(), deleted: false, comment_count: 0, description: "What's the deal with screenings?", @@ -33,7 +33,7 @@ export const questions = [ for (let i = 0; i < 20; i++) { questions.push({ - created_at: faker.date.past().toUTCString(), + created_at: faker.date.past().toISOString(), deleted: false, comment_count: 12, description: faker.lorem.sentence(), diff --git a/shared/mocks/data/research.ts b/shared/mocks/data/research.ts index ce60449692..c852931516 100644 --- a/shared/mocks/data/research.ts +++ b/shared/mocks/data/research.ts @@ -6,10 +6,10 @@ type ResearchSeed = Partial & { export const research: ResearchSeed[] = [ { - created_at: new Date('2022-03-27T22:08:25.999Z'), + created_at: '2022-03-27T22:08:25.999Z', created_by_username: 'event_reader', deleted: false, - modified_at: new Date('2022-03-27T22:10:11.271Z'), + modified_at: '2022-03-27T22:10:11.271Z', description: 'qwertyefew. Super qwerty. Gotta keep saying qwerty. qwerty qwerty. Qwerty!', slug: 'qwerty', previous_slugs: ['qwerty'], @@ -19,10 +19,10 @@ export const research: ResearchSeed[] = [ updates: [], }, { - created_at: new Date('2025-02-27T22:08:25.999Z'), + created_at: '2025-02-27T22:08:25.999Z', created_by_username: 'demo_user', deleted: false, - modified_at: new Date('2025-03-01T19:12:11.271Z'), + modified_at: '2025-03-01T19:12:11.271Z', description: 'A test!', slug: 'a-test-research', previous_slugs: [], @@ -32,10 +32,10 @@ export const research: ResearchSeed[] = [ updates: [], }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'demo_user', deleted: true, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'A deleted research test!', slug: 'a-deleted-test-research', previous_slugs: ['a-deleted-test-research'], @@ -44,10 +44,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'settings_workplace_new', deleted: true, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Workspace contribution tab test!', slug: 'workspace-test-research', previous_slugs: ['workspace-test-research'], @@ -56,10 +56,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'demo_user', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Workspace contribution tab test!', slug: 'a-research-test-2', previous_slugs: [], @@ -68,10 +68,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'demo_user', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Workspace contribution tab test!', slug: 'a-research-test-3', previous_slugs: [], @@ -80,10 +80,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'settings_workplace_new', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Workspace contribution tab test!', slug: 'a-research-test-4', previous_slugs: [], @@ -92,10 +92,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'demo_user', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Five is a magic number', slug: 'a-research-test-5', previous_slugs: [], @@ -104,10 +104,10 @@ export const research: ResearchSeed[] = [ is_draft: true, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'demo_user', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: '666', slug: 'a-research-test-6', previous_slugs: [], @@ -116,10 +116,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'demo_user', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Workspace contribution tab test!', slug: 'a-research-test-7', previous_slugs: [], @@ -128,10 +128,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'demo_user', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Workspace contribution tab test!', slug: 'a-research-test-8', status: 'in-progress', @@ -140,10 +140,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'demo_user', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Workspace contribution tab test!', slug: 'a-research-test-9', previous_slugs: [], @@ -152,10 +152,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'demo_user', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Workspace contribution tab test!', slug: 'a-research-test-10', previous_slugs: [], @@ -164,10 +164,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'event_reader', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Workspace contribution tab test!', slug: 'a-research-test-11', previous_slugs: [], @@ -176,10 +176,10 @@ export const research: ResearchSeed[] = [ is_draft: false, }, { - created_at: new Date('2023-02-27T22:08:25.999Z'), + created_at: '2023-02-27T22:08:25.999Z', created_by_username: 'event_reader', deleted: false, - modified_at: new Date('2023-03-01T19:12:11.271Z'), + modified_at: '2023-03-01T19:12:11.271Z', description: 'Workspace contribution tab test!', slug: 'a-research-test-12', previous_slugs: [], diff --git a/shared/mocks/data/researchUpdates.ts b/shared/mocks/data/researchUpdates.ts index 279e97b588..558ca1c3ef 100644 --- a/shared/mocks/data/researchUpdates.ts +++ b/shared/mocks/data/researchUpdates.ts @@ -2,9 +2,9 @@ import type { DBResearchUpdate } from '../../models'; export const researchUpdates: Partial[] = [ { - created_at: new Date('2022-03-27T22:10:11.271Z'), + created_at: '2022-03-27T22:10:11.271Z', deleted: false, - modified_at: new Date('2022-03-27T22:10:11.271Z'), + modified_at: '2022-03-27T22:10:11.271Z', description: 'qwerty', comment_count: 1, title: 'qwerty 1', @@ -18,25 +18,25 @@ export const researchUpdates: Partial[] = [ file_download_count: 2555, }, { - created_at: new Date('2022-04-27T22:10:11.271Z'), + created_at: '2022-04-27T22:10:11.271Z', deleted: false, - modified_at: new Date('2022-04-27T22:10:11.271Z'), + modified_at: '2022-04-27T22:10:11.271Z', description: 'qwerty', comment_count: 0, title: 'qwerty 2', }, { - created_at: new Date('2022-05-27T22:10:11.271Z'), + created_at: '2022-05-27T22:10:11.271Z', deleted: false, - modified_at: new Date('2022-05-27T22:10:11.271Z'), + modified_at: '2022-05-27T22:10:11.271Z', description: 'qwerty', comment_count: 0, title: 'qwerty 3', }, { - created_at: new Date('2022-06-27T22:10:11.271Z'), + created_at: '2022-06-27T22:10:11.271Z', deleted: false, - modified_at: new Date('2022-06-27T22:10:11.271Z'), + modified_at: '2022-06-27T22:10:11.271Z', description: 'qwerty', comment_count: 0, title: 'qwerty 4', diff --git a/shared/mocks/data/tags.ts b/shared/mocks/data/tags.ts index a5a77e509e..52d925772f 100644 --- a/shared/mocks/data/tags.ts +++ b/shared/mocks/data/tags.ts @@ -2,43 +2,43 @@ import type { DBTag } from '../../models/tag'; export const tags: Partial[] = [ { - created_at: new Date(), + created_at: new Date().toISOString(), name: 'product', }, { - created_at: new Date(), + created_at: new Date().toISOString(), name: 'exhibition', }, { - created_at: new Date(), + created_at: new Date().toISOString(), name: 'howto_testing', }, { - created_at: new Date(), + created_at: new Date().toISOString(), name: 'brainstorm', }, { - created_at: new Date(), + created_at: new Date().toISOString(), name: 'compression', }, { - created_at: new Date(), + created_at: new Date().toISOString(), name: 'mould', }, { - created_at: new Date(), + created_at: new Date().toISOString(), name: 'injection', }, { - created_at: new Date(), + created_at: new Date().toISOString(), name: 'workshop', }, { - created_at: new Date(), + created_at: new Date().toISOString(), name: 'extrusion', }, { - created_at: new Date(), + created_at: new Date().toISOString(), name: 'screening', }, ]; diff --git a/shared/models/banner.ts b/shared/models/banner.ts index 76815a9d3e..6a63aa4607 100644 --- a/shared/models/banner.ts +++ b/shared/models/banner.ts @@ -2,8 +2,8 @@ import type { IDBDocSB, IDoc } from './document'; export class DBBanner implements IDBDocSB { id: number; - created_at: Date; - modified_at: Date | null; + created_at: string; + modified_at: string | null; text: string; url: string; diff --git a/shared/models/category.ts b/shared/models/category.ts index d57661d79e..af3efd4afb 100644 --- a/shared/models/category.ts +++ b/shared/models/category.ts @@ -3,8 +3,8 @@ import type { IDBDocSB, IDoc } from './document'; export class DBCategory implements IDBDocSB { id: number; - created_at: Date; - modified_at: Date | null; + created_at: string; + modified_at: string | null; name: string; type: ContentType; diff --git a/shared/models/comment.ts b/shared/models/comment.ts index 5d0e2e8702..3f37f7a22a 100644 --- a/shared/models/comment.ts +++ b/shared/models/comment.ts @@ -5,8 +5,8 @@ import type { IDBDocSB, IDoc } from './document'; export class DBComment implements IDBDocSB { readonly id: number; - readonly created_at: Date; - readonly modified_at: Date | null; + readonly created_at: string; + readonly modified_at: string | null; readonly created_by: number | null; readonly deleted: boolean; diff --git a/shared/models/document.ts b/shared/models/document.ts index c888cc7fd8..6e1a8efab6 100644 --- a/shared/models/document.ts +++ b/shared/models/document.ts @@ -1,8 +1,8 @@ // Base level for all content on supabase export interface IDBDocSB { readonly id: number; - readonly created_at: Date; - readonly modified_at: Date | null; + readonly created_at: string; + readonly modified_at: string | null; } export interface IDoc { diff --git a/shared/models/library.ts b/shared/models/library.ts index 473fc9bac9..52aeb5e7ba 100644 --- a/shared/models/library.ts +++ b/shared/models/library.ts @@ -22,7 +22,7 @@ export const DifficultyLevelRecord: Record = { export class DBProject implements IDBContentDoc, IDBDownloadable, IDBModeration { readonly id: number; - readonly created_at: Date; + readonly created_at: string; readonly deleted: boolean | null; readonly author?: DBAuthor; readonly update_count?: number; @@ -34,7 +34,7 @@ export class DBProject implements IDBContentDoc, IDBDownloadable, IDBModeration readonly category: DBCategory | null; readonly steps: DBProjectStep[] | null; created_by: number | null; - modified_at: Date | null; + modified_at: string | null; title: string; slug: string; previous_slugs: string[] | null; diff --git a/shared/models/news.ts b/shared/models/news.ts index db5177eab9..2bc2f53c10 100644 --- a/shared/models/news.ts +++ b/shared/models/news.ts @@ -15,8 +15,8 @@ import type { Tag } from './tag'; export class DBNews implements IDBContentDoc { readonly id: number; - readonly created_at: Date; - readonly modified_at: Date | null; + readonly created_at: string; + readonly modified_at: string | null; readonly author?: DBAuthor; readonly comment_count?: number; readonly category: DBCategory | null; diff --git a/shared/models/profile.ts b/shared/models/profile.ts index 4dcf8bba79..1abe3a6979 100644 --- a/shared/models/profile.ts +++ b/shared/models/profile.ts @@ -17,7 +17,7 @@ import type { IUserImpact, UserVisitorPreference } from './user'; export class DBProfile { readonly id: number; - readonly created_at: Date; + readonly created_at: string; readonly tags?: DBProfileTagJoin[]; readonly badges?: DBProfileBadgeJoin[]; readonly pin?: DBMapPin; @@ -34,7 +34,7 @@ export class DBProfile { about: string | null; impact: string | null; is_contactable: boolean | null; - last_active: Date | null; + last_active: string | null; website: string | null; total_views: number; auth_id: string; @@ -90,7 +90,7 @@ export class Profile { return new Profile({ id: dbProfile.id, - createdAt: dbProfile.created_at, + createdAt: new Date(dbProfile.created_at), country: dbProfile.country, displayName: dbProfile.display_name, username: dbProfile.username, @@ -105,7 +105,7 @@ export class Profile { coverImages: coverImages ?? null, impact, isContactable: dbProfile.is_contactable || null, - lastActive: dbProfile.last_active, + lastActive: dbProfile.last_active ? new Date(dbProfile.last_active) : null, website: dbProfile.website, patreon: dbProfile.patreon ?? null, totalViews: dbProfile.total_views, @@ -137,9 +137,9 @@ export class DBNotification implements IDBDocSB { readonly action_type: NotificationActionType; readonly content_id: number; readonly content_type: NotificationContentType; - readonly created_at: Date; + readonly created_at: string; is_read: boolean; - modified_at: Date | null; + modified_at: string | null; readonly source_content_type: NotificationSourceContentType; readonly source_content_id: number; readonly owned_by: DBProfile; diff --git a/shared/models/profileTag.ts b/shared/models/profileTag.ts index b68803caeb..aebd4dc05d 100644 --- a/shared/models/profileTag.ts +++ b/shared/models/profileTag.ts @@ -2,7 +2,7 @@ export type ProfileCategory = 'member' | 'space'; export class DBProfileTag { id: number; - created_at: Date; + created_at: string; name: string; profile_type: string; diff --git a/shared/models/question.ts b/shared/models/question.ts index f58326e6c4..4188cd0fa1 100644 --- a/shared/models/question.ts +++ b/shared/models/question.ts @@ -11,8 +11,8 @@ import type { Tag } from './tag'; export class DBQuestion implements IDBContentDoc { readonly id: number; is_draft: boolean; - readonly created_at: Date; - readonly modified_at: Date | null; + readonly created_at: string; + readonly modified_at: string | null; readonly author?: DBAuthor; readonly comment_count?: number; readonly category: DBCategory | null; diff --git a/shared/models/research.ts b/shared/models/research.ts index 1b6488ce82..4fe2bd31bf 100644 --- a/shared/models/research.ts +++ b/shared/models/research.ts @@ -18,7 +18,7 @@ export const ResearchStatusRecord: Record = { export class DBResearchItem implements IDBContentDoc { readonly id: number; - readonly created_at: Date; + readonly created_at: string; readonly deleted: boolean | null; readonly author?: DBAuthor; readonly update_count?: number; @@ -30,7 +30,7 @@ export class DBResearchItem implements IDBContentDoc { readonly category: DBCategory | null; readonly updates: DBResearchUpdate[]; created_by: number | null; - modified_at: Date | null; + modified_at: string | null; title: string; slug: string; previous_slugs: string[] | null; @@ -135,14 +135,14 @@ export class ResearchItem implements IContentDoc { export class DBResearchUpdate implements IDBDocSB, IDBDownloadable { readonly id: number; readonly research_id: number; - readonly created_at: Date; + readonly created_at: string; readonly deleted: boolean | null; readonly is_draft: boolean | null; readonly comment_count?: number; readonly file_download_count?: number; readonly update_author?: DBAuthor; created_by: number | null; - modified_at: Date | null; + modified_at: string | null; title: string; description: string; images: DBMedia[] | null; diff --git a/shared/models/tag.ts b/shared/models/tag.ts index 90ee243d94..4d17fcbb98 100644 --- a/shared/models/tag.ts +++ b/shared/models/tag.ts @@ -2,8 +2,8 @@ import type { IDBDocSB, IDoc } from './document'; export class DBTag implements IDBDocSB { id: number; - created_at: Date; - modified_at: Date | null; + created_at: string; + modified_at: string | null; name: string; diff --git a/src/routes/_.library.$slug._index.tsx b/src/routes/_.library.$slug._index.tsx index f840d88442..15b171d81a 100644 --- a/src/routes/_.library.$slug._index.tsx +++ b/src/routes/_.library.$slug._index.tsx @@ -11,6 +11,7 @@ import { ImageServiceServer } from 'src/services/imageService.server'; import { libraryServiceServer } from 'src/services/libraryService.server'; import { TenantSettingsService } from 'src/services/tenantSettingsService.server'; import { generateTags, mergeMeta } from 'src/utils/seo.utils'; +import { dbResult } from 'src/utils/supabase.types'; export async function loader({ request, params }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -21,7 +22,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return data({ project: null }, { headers }); } - const dbProject = result.data as unknown as DBProject; + const dbProject = dbResult(result.data); if (dbProject.id) { await contentServiceServer.incrementViewCount( diff --git a/src/routes/_.library.$slug.edit.tsx b/src/routes/_.library.$slug.edit.tsx index 37c2841d0a..7bda64d821 100644 --- a/src/routes/_.library.$slug.edit.tsx +++ b/src/routes/_.library.$slug.edit.tsx @@ -8,6 +8,7 @@ import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { libraryServiceServer } from 'src/services/libraryService.server'; import { redirectServiceServer } from 'src/services/redirectService.server'; import { storageServiceServer } from 'src/services/storageService.server'; +import { dbResult } from 'src/utils/supabase.types'; export async function loader({ request, params }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -20,8 +21,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // const username = user.user_metadata.username const username = claims.data.claims.user_metadata?.username; - const projectDb = (await libraryServiceServer.getBySlug(client, params.slug as string)) - .data as unknown as DBProject; + const projectDb = dbResult( + (await libraryServiceServer.getBySlug(client, params.slug as string)).data, + ); if ( !(await libraryServiceServer.isAllowedToEditProject( client, diff --git a/src/routes/_.news.$slug._index.tsx b/src/routes/_.news.$slug._index.tsx index bd079da13c..42cae49f0e 100644 --- a/src/routes/_.news.$slug._index.tsx +++ b/src/routes/_.news.$slug._index.tsx @@ -12,6 +12,7 @@ import { ProfileServiceServer } from 'src/services/profileService.server'; import { redirectServiceServer } from 'src/services/redirectService.server'; import { TenantSettingsService } from 'src/services/tenantSettingsService.server'; import { generateTags, mergeMeta } from 'src/utils/seo.utils'; +import { dbResult } from 'src/utils/supabase.types'; import { contentServiceServer } from '../services/contentService.server'; export async function loader({ request, params }: LoaderFunctionArgs) { @@ -24,7 +25,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return data({ news: null, tenantSettings }, { headers }); } - const dbNews = result.data as unknown as DBNews; + const dbNews = dbResult(result.data); const profileBadgeId = dbNews.profile_badge?.id; if (!profileBadgeId) { diff --git a/src/routes/_.news.$slug.edit.tsx b/src/routes/_.news.$slug.edit.tsx index 51dd3f1418..cdfb168dc4 100644 --- a/src/routes/_.news.$slug.edit.tsx +++ b/src/routes/_.news.$slug.edit.tsx @@ -8,6 +8,7 @@ import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { newsServiceServer } from 'src/services/newsService.server'; import { redirectServiceServer } from 'src/services/redirectService.server'; import { tagsServiceServer } from 'src/services/tagsService.server'; +import { dbResult } from 'src/utils/supabase.types'; export async function loader({ request, params }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -32,7 +33,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return data({ news: null }, { headers }); } - const dbNews = result.data as unknown as DBNews; + const dbNews = dbResult(result.data); const tags = await tagsServiceServer.getTags(client, dbNews.tags); const heroImage = await newsServiceServer.getHeroImage(client, dbNews.hero_image); const news = News.fromDB(dbNews, tags, heroImage); diff --git a/src/routes/_.questions.$slug._index.tsx b/src/routes/_.questions.$slug._index.tsx index cfa1244ee9..edb987a77e 100644 --- a/src/routes/_.questions.$slug._index.tsx +++ b/src/routes/_.questions.$slug._index.tsx @@ -13,6 +13,7 @@ import { storageServiceServer } from 'src/services/storageService.server'; import { TenantSettingsService } from 'src/services/tenantSettingsService.server'; import { generateTags, mergeMeta } from 'src/utils/seo.utils'; import { contentServiceServer } from '../services/contentService.server'; +import { dbResult } from 'src/utils/supabase.types'; export async function loader({ request, params }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -25,7 +26,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return data({ question: null, tenantSettings }, { headers }); } - const dbQuestion = result.data as unknown as DBQuestion; + const dbQuestion = dbResult(result.data); if (dbQuestion.id) { await contentServiceServer.incrementViewCount(client, 'questions', dbQuestion.total_views, dbQuestion.id); diff --git a/src/routes/_.questions.$slug.edit.tsx b/src/routes/_.questions.$slug.edit.tsx index 1bbc06eac7..4acc228ba5 100644 --- a/src/routes/_.questions.$slug.edit.tsx +++ b/src/routes/_.questions.$slug.edit.tsx @@ -9,6 +9,7 @@ import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { questionServiceServer } from 'src/services/questionService.server'; import { redirectServiceServer } from 'src/services/redirectService.server'; import { storageServiceServer } from 'src/services/storageService.server'; +import { dbResult } from 'src/utils/supabase.types'; export async function loader({ request, params }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -29,7 +30,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return data({ question: null }, { headers }); } - const dbQuestion = result.data as unknown as DBQuestion; + const dbQuestion = dbResult(result.data); if (!(await isUserAllowedToEdit(dbQuestion, claims.data.claims.sub, client))) { return redirect('/forbidden?page=question-edit', { headers }); diff --git a/src/routes/_.research.$slug.edit.tsx b/src/routes/_.research.$slug.edit.tsx index 7199288fc0..f2abf50205 100644 --- a/src/routes/_.research.$slug.edit.tsx +++ b/src/routes/_.research.$slug.edit.tsx @@ -6,6 +6,7 @@ import ResearchForm from 'src/pages/Research/Content/Common/ResearchForm'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { redirectServiceServer } from 'src/services/redirectService.server'; import { researchServiceServer } from 'src/services/researchService.server'; +import { dbResult } from 'src/utils/supabase.types'; export async function loader({ request, params }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -23,7 +24,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } const currentUsername = claims.data.claims.user_metadata?.username; - const researchDb = result.item as unknown as DBResearchItem; + const researchDb = dbResult(result.item); const images = researchServiceServer.getResearchPublicMedia(researchDb, client); const research = ResearchItem.fromDB(researchDb, [], images, currentUsername); diff --git a/src/routes/_.research.$slug.new-update.tsx b/src/routes/_.research.$slug.new-update.tsx index 5a631be0a0..443783775a 100644 --- a/src/routes/_.research.$slug.new-update.tsx +++ b/src/routes/_.research.$slug.new-update.tsx @@ -6,6 +6,7 @@ import { ResearchUpdateForm } from 'src/pages/Research/Content/Common'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { redirectServiceServer } from 'src/services/redirectService.server'; import { researchServiceServer } from 'src/services/researchService.server'; +import { dbResult } from 'src/utils/supabase.types'; export async function loader({ request, params }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -23,7 +24,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } const username = claims.data.claims.user_metadata?.username; - const researchDb = result.item as unknown as DBResearchItem; + const researchDb = dbResult(result.item); const research = ResearchItem.fromDB(researchDb, [], [], result.collaborators); if (!(await researchServiceServer.isAllowedToEditResearch(client, research, username))) { diff --git a/src/routes/api.categories.$type.ts b/src/routes/api.categories.$type.ts index 8480afb317..9d20d8a92c 100644 --- a/src/routes/api.categories.$type.ts +++ b/src/routes/api.categories.$type.ts @@ -4,6 +4,7 @@ import { Category } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; import { isProductionEnvironment } from 'src/config/config'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; +import { dbResult } from 'src/utils/supabase.types'; const cache = new Keyv({ ttl: 3600000 }); // ttl: 60 minutes @@ -33,7 +34,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const { data } = await client.from('categories').select('id,name,created_at,type'); - const categories = data?.map((category) => Category.fromDB(category as unknown as DBCategory)); + const categories = data?.map((category) => Category.fromDB(dbResult(category))); if (categories && categories.length > 0) { cache.set('categories', data, 3600000); diff --git a/src/routes/api.discussions.$sourceId.comments.$id.ts b/src/routes/api.discussions.$sourceId.comments.$id.ts index 4b1035c82e..e43110f3f5 100644 --- a/src/routes/api.discussions.$sourceId.comments.$id.ts +++ b/src/routes/api.discussions.$sourceId.comments.$id.ts @@ -4,6 +4,7 @@ import { UserRole } from 'oa-shared'; import type { LoaderFunctionArgs, Params } from 'react-router'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { updateUserActivity } from 'src/utils/activity.server'; +import { dbResult } from 'src/utils/supabase.types'; type Supabase = { headers: Headers; @@ -63,7 +64,7 @@ async function updateComment( return Response.json({}, { headers, status: 404, statusText: 'comment not found' }); } - const comment = data as unknown as DBComment; + const comment = dbResult(data); if (comment.created_by !== user.id && !isUserAdmin(user)) { return Response.json({}, { headers, status: 403, statusText: 'forbidden' }); @@ -88,7 +89,7 @@ async function deleteComment({ client, headers }: Supabase, id: string, user: DB return Response.json({}, { headers, status: 404, statusText: 'comment not found' }); } - const comment = data as unknown as DBComment; + const comment = dbResult(data); if (comment.created_by !== user.id && !isUserAdmin(user)) { return Response.json({}, { headers, status: 403, statusText: 'forbidden' }); @@ -115,7 +116,7 @@ async function getProfileByAuthId(request: Request, authId: string) { return null; } - return data[0] as unknown as DBProfile; + return dbResult(data[0]); } function isUserAdmin(user: DBProfile) { diff --git a/src/routes/api.discussions.$sourceType.$sourceId.comments.ts b/src/routes/api.discussions.$sourceType.$sourceId.comments.ts index b1da909c6f..3bcaf0b725 100644 --- a/src/routes/api.discussions.$sourceType.$sourceId.comments.ts +++ b/src/routes/api.discussions.$sourceType.$sourceId.comments.ts @@ -8,6 +8,7 @@ import { ImageServiceServer } from 'src/services/imageService.server'; import { notificationsSupabaseServiceServer } from 'src/services/notificationsSupabaseService.server'; import { subscribersServiceServer } from 'src/services/subscribersService.server'; import { updateUserActivity } from 'src/utils/activity.server'; +import { dbResult, dbResultArray } from 'src/utils/supabase.types'; export async function loader({ params, request }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -38,7 +39,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { p_current_user_id: currentUserId || null, }); - const dbComments = result.data as unknown as DBComment[]; + const dbComments = dbResultArray(result.data); const commentFactory = new CommentFactory(new ImageServiceServer(client)); const comments = await commentFactory.fromDBCommentsToThreads(dbComments); @@ -128,8 +129,8 @@ export async function action({ params, request }: LoaderFunctionArgs) { .single(); if (!commentResult.error) { - const comment = commentResult.data as unknown as DBComment; - const profile = currentUser.data[0] as unknown as DBProfile; + const comment = dbResult(commentResult.data); + const profile = dbResult(currentUser.data[0]); addSubscriptions(comment, profile, client, headers); @@ -137,7 +138,7 @@ export async function action({ params, request }: LoaderFunctionArgs) { } const commentDb = new DBComment({ - ...(commentResult.data as unknown as DBComment), + ...dbResult(commentResult.data), profile: (commentResult.data as any).profiles as DBAuthor, }); diff --git a/src/routes/api.donation-settings.$profileId.ts b/src/routes/api.donation-settings.$profileId.ts index 4c10074045..376208ee1b 100644 --- a/src/routes/api.donation-settings.$profileId.ts +++ b/src/routes/api.donation-settings.$profileId.ts @@ -2,6 +2,7 @@ import type { DBMedia } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ImageServiceServer } from 'src/services/imageService.server'; +import { fromJson } from 'src/utils/supabase.types'; export async function loader({ request, params }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -33,7 +34,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (profileResult.data.cover_images?.at(0)) { donationSettings.imageUrl = new ImageServiceServer(client).getPublicUrl( - profileResult.data.cover_images[0] as unknown as DBMedia | null, + fromJson(profileResult.data.cover_images[0]), )?.publicUrl; } } diff --git a/src/routes/api.map-filters.ts b/src/routes/api.map-filters.ts index 96af371ab8..d13cfec96e 100644 --- a/src/routes/api.map-filters.ts +++ b/src/routes/api.map-filters.ts @@ -11,6 +11,7 @@ import type { import { ProfileBadge, ProfileTag, ProfileType } from 'oa-shared'; import { isProductionEnvironment } from 'src/config/config'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; +import { dbResult } from 'src/utils/supabase.types'; const cache = new Keyv({ ttl: 3600000 }); // expires 60 minutes after being set @@ -38,12 +39,12 @@ export const loader = async ({ request }) => { console.error({ message: 'Error fetching map pin filters', errors }); } - const settings = mapSettings?.data?.at(0) as unknown as DBMapSettings | undefined; + const settings = dbResult(mapSettings?.data?.at(0)); const filters: MapFilters = { - tags: tags?.data?.map((x) => ProfileTag.fromDB(x as unknown as DBProfileTag)), - badges: badges?.data?.map((x) => ProfileBadge.fromDB(x as unknown as DBProfileBadge)), - types: types?.data?.map((x) => ProfileType.fromDB(x as unknown as DBProfileType)), + tags: tags?.data?.map((x) => ProfileTag.fromDB(dbResult(x))), + badges: badges?.data?.map((x) => ProfileBadge.fromDB(dbResult(x))), + types: types?.data?.map((x) => ProfileType.fromDB(dbResult(x))), settings: settings?.setting_filters || undefined, }; diff --git a/src/routes/api.map-pin.ts b/src/routes/api.map-pin.ts index cfc952ba9f..380adf63d2 100644 --- a/src/routes/api.map-pin.ts +++ b/src/routes/api.map-pin.ts @@ -2,6 +2,7 @@ import type { DBMapPin } from 'oa-shared'; import { MapPinFactory } from 'src/factories/mapPinFactory.server'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; +import { dbResult } from 'src/utils/supabase.types'; export const loader = async ({ request }) => { const { client, headers } = createSupabaseServerClient(request); @@ -82,7 +83,7 @@ export const loader = async ({ request }) => { return Response.json({ mapPin: null }, { headers }); } - const pinsDb = data[0] as unknown as DBMapPin; + const pinsDb = dbResult(data[0]); const pinFactory = new MapPinFactory(client); const mapPin = pinFactory.fromDBWithProfile(pinsDb); diff --git a/src/routes/api.map-pins.$userId.ts b/src/routes/api.map-pins.$userId.ts index 4620400aaf..7352ab115b 100644 --- a/src/routes/api.map-pins.$userId.ts +++ b/src/routes/api.map-pins.$userId.ts @@ -1,6 +1,7 @@ import type { DBMapPin } from 'oa-shared'; import { MapPinFactory } from 'src/factories/mapPinFactory.server'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; +import { dbResult } from 'src/utils/supabase.types'; // runs on the server export const loader = async ({ request, params }) => { @@ -72,7 +73,7 @@ export const loader = async ({ request, params }) => { return Response.json({ mapPin: null }, { headers }); } - const pinsDb = data[0] as unknown as DBMapPin; + const pinsDb = dbResult(data[0]); const pinFactory = new MapPinFactory(client); const mapPin = pinFactory.fromDBWithProfile(pinsDb); diff --git a/src/routes/api.news.$id.ts b/src/routes/api.news.$id.ts index 02be177fc7..c57e801571 100644 --- a/src/routes/api.news.$id.ts +++ b/src/routes/api.news.$id.ts @@ -3,7 +3,6 @@ import type { DBNews } from 'oa-shared'; import { News } from 'oa-shared'; import type { LoaderFunctionArgs, Params } from 'react-router'; import { IMAGE_SIZES } from 'src/config/imageTransforms'; -import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { contentServiceServer } from 'src/services/contentService.server'; import { newsServiceServer } from 'src/services/newsService.server'; @@ -14,6 +13,7 @@ import { getSummaryFromMarkdown } from 'src/utils/getSummaryFromMarkdown'; import { hasAdminRights } from 'src/utils/helpers'; import { convertToSlug } from 'src/utils/slug'; import { validateImage } from 'src/utils/storage'; +import { dbResult, toJson } from 'src/utils/supabase.types'; export const action = async ({ request, params }: LoaderFunctionArgs) => { const { client, headers } = createSupabaseServerClient(request); @@ -91,7 +91,7 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { throw newsResult.error; } - const news = News.fromDB(newsResult.data[0] as unknown as DBNews, []); + const news = News.fromDB(dbResult(newsResult.data[0]), []); if (newHeroImage) { const mediaFiles = await storageServiceServer.uploadImage( @@ -104,7 +104,7 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { await client .from('news') .update({ - hero_image: mediaFiles.media.at(0) as unknown as Json, + hero_image: toJson(mediaFiles.media.at(0)), }) .eq('id', news.id); diff --git a/src/routes/api.news.drafts.ts b/src/routes/api.news.drafts.ts index f87383ee68..584b0390ef 100644 --- a/src/routes/api.news.drafts.ts +++ b/src/routes/api.news.drafts.ts @@ -5,6 +5,7 @@ import { IMAGE_SIZES } from 'src/config/imageTransforms'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; import { storageServiceServer } from 'src/services/storageService.server'; +import { dbResultArray } from 'src/utils/supabase.types'; export async function loader({ request }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -38,7 +39,7 @@ export async function loader({ request }: LoaderFunctionArgs) { return Response.json({ items: [] }, { headers }); } - const drafts = result.data as unknown as DBNews[]; + const drafts = dbResultArray(result.data); const items = drafts.map((x) => { const images = x.hero_image ? storageServiceServer.getPublicUrls(client, [x.hero_image], IMAGE_SIZES.GALLERY) diff --git a/src/routes/api.news.ts b/src/routes/api.news.ts index 7529f94738..b04bccaff9 100644 --- a/src/routes/api.news.ts +++ b/src/routes/api.news.ts @@ -4,7 +4,6 @@ import type { AuthError } from '@supabase/supabase-js'; import type { DBNews, DBProfile, Moderation } from 'oa-shared'; import { News } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; -import type { Json } from 'src/database.types'; import { ITEMS_PER_PAGE } from 'src/pages/News/constants'; import type { NewsSortOption } from 'src/pages/News/NewsSortOptions'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; @@ -17,6 +16,7 @@ import { updateUserActivity } from 'src/utils/activity.server'; import { getSummaryFromMarkdown } from 'src/utils/getSummaryFromMarkdown'; import { convertToSlug } from 'src/utils/slug'; import { validateImage } from 'src/utils/storage'; +import { dbResult, dbResultArray, toJson } from 'src/utils/supabase.types'; import { contentServiceServer } from '../services/contentService.server'; export const loader = async ({ request }) => { @@ -90,7 +90,7 @@ export const loader = async ({ request }) => { const queryResult = await query.range(skip, skip + ITEMS_PER_PAGE - 1); // 0 based const total = queryResult.count; - const data = queryResult.data as unknown as DBNews[]; + const data = dbResultArray(queryResult.data); const items = data.map((dbNews) => News.fromDB(dbNews, [])); if (items && items.length > 0) { @@ -208,7 +208,7 @@ export const action = async ({ request }: LoaderFunctionArgs) => { throw newsResult.error; } - const news = News.fromDB(newsResult.data[0] as unknown as DBNews, []); + const news = News.fromDB(dbResult(newsResult.data[0]), []); subscribersServiceServer.add('news', news.id, profile.id, client, headers); if (!news.isDraft) { @@ -226,7 +226,7 @@ export const action = async ({ request }: LoaderFunctionArgs) => { await client .from('news') .update({ - hero_image: mediaFiles.media.at(0) as unknown as Json, + hero_image: toJson(mediaFiles.media.at(0)), }) .eq('id', news.id); diff --git a/src/routes/api.notifications.ts b/src/routes/api.notifications.ts index e857adb0ce..81ae120a03 100644 --- a/src/routes/api.notifications.ts +++ b/src/routes/api.notifications.ts @@ -3,6 +3,7 @@ import type { DBNotification } from 'oa-shared'; import { Notification, NotificationDisplay } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; +import { dbResultArray } from 'src/utils/supabase.types'; const transformNotificationList = async ( dbNotifications: DBNotification[], @@ -76,7 +77,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { } const notifications = data?.length - ? await transformNotificationList(data as unknown as DBNotification[], client) + ? await transformNotificationList(dbResultArray(data), client) : []; return Response.json({ notifications }, { headers, status: 200 }); diff --git a/src/routes/api.profile-tags.ts b/src/routes/api.profile-tags.ts index edb012455d..abd8a5afa5 100644 --- a/src/routes/api.profile-tags.ts +++ b/src/routes/api.profile-tags.ts @@ -2,6 +2,7 @@ import type { DBProfileTag } from 'oa-shared'; import { ProfileTag } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; +import { dbResultArray } from 'src/utils/supabase.types'; export async function loader({ request }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -10,7 +11,7 @@ export async function loader({ request }: LoaderFunctionArgs) { .from('profile_tags') .select('id,created_at,name,profile_type'); - const dbTags = (profileTagsResult.data || []) as unknown as DBProfileTag[]; + const dbTags = dbResultArray(profileTagsResult.data || []); const tags = dbTags.map((x) => ProfileTag.fromDB(x)); return Response.json(tags, { headers, status: 200 }); diff --git a/src/routes/api.profile.ts b/src/routes/api.profile.ts index 4921cf9b86..dbfa5fbe76 100644 --- a/src/routes/api.profile.ts +++ b/src/routes/api.profile.ts @@ -5,6 +5,7 @@ import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; import { ProfileTypesServiceServer } from 'src/services/profileTypesService.server'; import { updateUserActivity } from 'src/utils/activity.server'; +import { dbResult } from 'src/utils/supabase.types'; export const loader = async ({ request }) => { const { client, headers } = createSupabaseServerClient(request); @@ -58,7 +59,7 @@ export const loader = async ({ request }) => { } const profileFactory = new ProfileFactory(client); - const profile = profileFactory.fromDB(data as unknown as DBProfile); + const profile = profileFactory.fromDB(dbResult(data)); return Response.json(profile, { headers, status: 200 }); } catch (error) { diff --git a/src/routes/api.profiles.ts b/src/routes/api.profiles.ts index 8f04d964da..102bed8587 100644 --- a/src/routes/api.profiles.ts +++ b/src/routes/api.profiles.ts @@ -1,6 +1,7 @@ import type { DBProfile } from 'oa-shared'; import { ProfileFactory } from 'src/factories/profileFactory.server'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; +import { dbResult } from 'src/utils/supabase.types'; export const loader = async ({ request }) => { const { client, headers } = createSupabaseServerClient(request); @@ -43,7 +44,7 @@ export const loader = async ({ request }) => { .limit(10); const profileFactory = new ProfileFactory(client); - const profiles = data?.map((x) => profileFactory.fromDB(x as unknown as DBProfile)); + const profiles = data?.map((x) => profileFactory.fromDB(dbResult(x))); return Response.json(profiles, { headers, status: 200 }); }; diff --git a/src/routes/api.projects.$id.ts b/src/routes/api.projects.$id.ts index 2f5bb43823..a189926190 100644 --- a/src/routes/api.projects.$id.ts +++ b/src/routes/api.projects.$id.ts @@ -10,6 +10,7 @@ import { ProfileServiceServer } from 'src/services/profileService.server'; import { storageServiceServer } from 'src/services/storageService.server'; import { updateUserActivity } from 'src/utils/activity.server'; import { convertToSlug } from 'src/utils/slug'; +import { dbResult, fromJsonArray, toJson } from 'src/utils/supabase.types'; export const action = async ({ request, params }: ActionFunctionArgs) => { const { client, headers } = createSupabaseServerClient(request); @@ -138,11 +139,11 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const { data } = await client .from('project_steps') - .update({ images: images as unknown as Json[] }) + .update({ images: dbResult(images) }) .eq('id', stepDb.id) .select('images') .single(); - step.images = data?.images as unknown as Image[]; + step.images = fromJsonArray(data?.images); project.steps.push(step); } @@ -254,9 +255,9 @@ async function updateProject( file_link: data.fileLink, difficulty_level: data.difficultyLevel, time: data.time, - files: files as unknown as Json[], + files: dbResult(files), moderation, - cover_image: cover_image as unknown as Json, + cover_image: toJson(cover_image), }) .eq('id', currentProject.id) .select(); @@ -265,7 +266,7 @@ async function updateProject( throw projectResult.error; } - return projectResult.data[0] as unknown as DBProject; + return dbResult(projectResult.data[0]); } async function updateOrReplaceImages( @@ -285,7 +286,7 @@ async function updateOrReplaceImages( .single(); if (existingMedia.data && existingMedia.data.images?.length > 0) { - media = (existingMedia.data.images as unknown as DBMedia[]).filter((x) => + media = fromJsonArray(existingMedia.data.images).filter((x) => idsToKeep.includes(x.id), ); } diff --git a/src/routes/api.projects.drafts.ts b/src/routes/api.projects.drafts.ts index c8962371c9..a82f526726 100644 --- a/src/routes/api.projects.drafts.ts +++ b/src/routes/api.projects.drafts.ts @@ -5,6 +5,7 @@ import { IMAGE_SIZES } from 'src/config/imageTransforms'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; import { storageServiceServer } from 'src/services/storageService.server'; +import { dbResultArray } from 'src/utils/supabase.types'; export async function loader({ request }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -68,7 +69,7 @@ export async function loader({ request }: LoaderFunctionArgs) { return Response.json({ items: [] }, { headers }); } - const drafts = result.data as unknown as DBProject[]; + const drafts = dbResultArray(result.data); const items = drafts.map((x) => { const images = x.cover_image ? storageServiceServer.getPublicUrls(client, [x.cover_image], IMAGE_SIZES.LIST) diff --git a/src/routes/api.projects.ts b/src/routes/api.projects.ts index 26013555d2..f1ae902ed4 100644 --- a/src/routes/api.projects.ts +++ b/src/routes/api.projects.ts @@ -14,6 +14,7 @@ import { subscribersServiceServer } from 'src/services/subscribersService.server import { updateUserActivity } from 'src/utils/activity.server'; import { convertToSlug } from 'src/utils/slug'; import { validateImage } from 'src/utils/storage'; +import { dbResult, dbResultArray, fromJsonArray } from 'src/utils/supabase.types'; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); @@ -49,7 +50,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { return Response.json({}, { status: 500, headers }); } - const dbItems = data as unknown as DBProject[]; + const dbItems = dbResultArray(data); const items = dbItems.map((x) => { const images = x.cover_image ? storageServiceServer.getPublicUrls(client, [x.cover_image], IMAGE_SIZES.LIST) @@ -264,7 +265,7 @@ async function createProject( throw projectResult.error; } - return projectResult.data[0] as unknown as DBProject; + return dbResult(projectResult.data[0]); } async function createStep( @@ -293,7 +294,7 @@ async function createStep( throw error; } - return data[0] as unknown as DBProjectStep; + return dbResult(data[0]); } async function uploadAndUpdateImage( @@ -333,13 +334,13 @@ async function uploadAndUpdateFiles( const result = await client .from('projects') .update({ - files: mediaResult.media as unknown as Json[], + files: dbResult(mediaResult.media), }) .eq('id', project.id) .select(); if (result.data) { - project.files = result.data[0].files as unknown as MediaFile[]; + project.files = fromJsonArray(result.data[0].files); } } } diff --git a/src/routes/api.questions.$id.ts b/src/routes/api.questions.$id.ts index c4e18ee588..d8af65a0c1 100644 --- a/src/routes/api.questions.$id.ts +++ b/src/routes/api.questions.$id.ts @@ -12,6 +12,7 @@ import { updateUserActivity } from 'src/utils/activity.server'; import { hasAdminRights } from 'src/utils/helpers'; import { convertToSlug } from 'src/utils/slug'; import { validateImages } from 'src/utils/storage'; +import { dbResult, fromJsonArray } from 'src/utils/supabase.types'; import { contentServiceServer } from '../services/contentService.server'; export const action = async ({ request, params }: LoaderFunctionArgs) => { @@ -81,7 +82,7 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { questionImages.data.images && questionImages.data.images.length > 0 ) { - images = (questionImages.data.images as unknown as DBMedia[]).filter((x) => + images = fromJsonArray(questionImages.data.images).filter((x) => imagesToKeepIds.includes(x.id), ); } @@ -107,7 +108,7 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { category: data.category, description: data.description, is_draft: data.is_draft, - images: images as unknown as Json[], + images: dbResult(images), title: data.title, slug: data.slug, previous_slugs: previousSlugs, @@ -123,12 +124,12 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => { const newImages = storageServiceServer.getPublicUrls( client, - questionResult.data[0].images as unknown as DBMedia[], + fromJsonArray(questionResult.data[0].images), IMAGE_SIZES.GALLERY, ); const question = Question.fromDB( - questionResult.data[0] as unknown as DBQuestion, + dbResult(questionResult.data[0]), [], newImages, ); diff --git a/src/routes/api.questions.drafts.ts b/src/routes/api.questions.drafts.ts index b3d14f1800..73b3539e8a 100644 --- a/src/routes/api.questions.drafts.ts +++ b/src/routes/api.questions.drafts.ts @@ -5,6 +5,7 @@ import { IMAGE_SIZES } from 'src/config/imageTransforms'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; import { storageServiceServer } from 'src/services/storageService.server'; +import { dbResultArray } from 'src/utils/supabase.types'; export async function loader({ request }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -59,7 +60,7 @@ export async function loader({ request }: LoaderFunctionArgs) { return Response.json({ items: [] }, { headers }); } - const drafts = result.data as unknown as DBQuestion[]; + const drafts = dbResultArray(result.data); const items = drafts.map((x) => { const images = x.images ? storageServiceServer.getPublicUrls(client, x.images, IMAGE_SIZES.LIST) diff --git a/src/routes/api.questions.ts b/src/routes/api.questions.ts index 9b58784cf8..c02511eebb 100644 --- a/src/routes/api.questions.ts +++ b/src/routes/api.questions.ts @@ -14,6 +14,7 @@ import { subscribersServiceServer } from 'src/services/subscribersService.server import { updateUserActivity } from 'src/utils/activity.server'; import { convertToSlug } from 'src/utils/slug'; import { validateImages } from 'src/utils/storage'; +import { dbResult, dbResultArray, fromJsonArray } from 'src/utils/supabase.types'; export const loader = async ({ request }) => { const url = new URL(request.url); @@ -74,7 +75,7 @@ export const loader = async ({ request }) => { const queryResult = await query.range(skip, skip + ITEMS_PER_PAGE); // 0 based const total = queryResult.count; - const data = queryResult.data as unknown as DBQuestion[]; + const data = dbResultArray(queryResult.data); const items = data.map((x) => Question.fromDB(x, [], [])); @@ -184,7 +185,7 @@ export const action = async ({ request }: LoaderFunctionArgs) => { return Response.json({}, { headers, status: 400, statusText: questionResult.error.details }); } - const question = Question.fromDB(questionResult.data[0] as unknown as DBQuestion, []); + const question = Question.fromDB(dbResult(questionResult.data[0]), []); subscribersServiceServer.add('questions', question.id, profile.id, client, headers); if (uploadedImages.length > 0) { @@ -199,12 +200,12 @@ export const action = async ({ request }: LoaderFunctionArgs) => { if (imageResult?.media && imageResult.media.length > 0) { const updateResult = await client .from('questions') - .update({ images: imageResult.media as unknown as Json[] }) + .update({ images: dbResult(imageResult.media) }) .eq('id', questionId) .select(); if (updateResult.data) { - question.images = updateResult.data[0].images as unknown as Image[]; + question.images = fromJsonArray(updateResult.data[0].images); } } } diff --git a/src/routes/api.research.$id.ts b/src/routes/api.research.$id.ts index 436e54d257..aaf5aa628d 100644 --- a/src/routes/api.research.$id.ts +++ b/src/routes/api.research.$id.ts @@ -2,7 +2,6 @@ import type { SupabaseClient } from '@supabase/supabase-js'; import type { DBMedia, DBResearchItem } from 'oa-shared'; import { ResearchItem, UserRole } from 'oa-shared'; import type { ActionFunctionArgs } from 'react-router'; -import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { contentServiceServer } from 'src/services/contentService.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; @@ -11,6 +10,7 @@ import { storageServiceServer } from 'src/services/storageService.server'; import { subscribersServiceServer } from 'src/services/subscribersService.server'; import { updateUserActivity } from 'src/utils/activity.server'; import { convertToSlug } from 'src/utils/slug'; +import { dbResult, fromJson, toJson } from 'src/utils/supabase.types'; export const action = async ({ request, params }: ActionFunctionArgs) => { const id = Number(params.id); @@ -66,7 +66,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { description: data.description, slug: data.slug, category: data.category, - tags: data.tags as unknown as string[], + tags: dbResult(data.tags), previous_slugs: previousSlugs, is_draft: data.isDraft, collaborators: data.collaborators, @@ -80,7 +80,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { throw researchResult.error; } - const research = ResearchItem.fromDB(researchResult.data as unknown as DBResearchItem, []); + const research = ResearchItem.fromDB(dbResult(researchResult.data), []); await subscribersServiceServer.updateResearchSubscribers( oldResearch, @@ -104,14 +104,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { if (mediaResult?.media && mediaResult.media.length > 0) { const result = await client .from('research') - .update({ image: mediaResult.media[0] as unknown as Json }) + .update({ image: toJson(mediaResult.media[0]) }) .eq('id', research.id) .select('image'); if (result.data && result.data.length > 0) { const [image] = storageServiceServer.getPublicUrls( client, - result.data?.at(0)?.image ? [result.data[0].image as unknown as DBMedia] : [], + result.data?.at(0)?.image ? [fromJson(result.data[0].image)!] : [], ); research.image = image; diff --git a/src/routes/api.research.$id.updates.$updateId.ts b/src/routes/api.research.$id.updates.$updateId.ts index c25a752314..a01d5d1825 100644 --- a/src/routes/api.research.$id.updates.$updateId.ts +++ b/src/routes/api.research.$id.updates.$updateId.ts @@ -10,6 +10,7 @@ import { researchServiceServer } from 'src/services/researchService.server'; import { storageServiceServer } from 'src/services/storageService.server'; import { updateUserActivity } from 'src/utils/activity.server'; import { validateImages } from 'src/utils/storage'; +import { dbResult, fromJsonArray } from 'src/utils/supabase.types'; export const action = async ({ request, params }: ActionFunctionArgs) => { const { client, headers } = createSupabaseServerClient(request); @@ -59,7 +60,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { .eq('id', updateId) .single(); - const oldResearchUpdate = researchUpdateResult.data as unknown as DBResearchUpdate; + const oldResearchUpdate = dbResult(researchUpdateResult.data); const uploadedImages = formData.getAll('images') as File[]; const uploadedFiles = formData.getAll('files') as File[]; @@ -99,7 +100,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { title: data.title, description: data.description, is_draft: data.isDraft, - images: images as unknown as Json[], + images: dbResult(images), modified_at: new Date().toISOString(), video_url: data.videoUrl, files: files.map((x) => ({ id: x.id, name: x.name, size: x.size })), @@ -113,11 +114,12 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { } const researchUpdate = ResearchUpdate.fromDB( - researchUpdateAfterUpdating.data as unknown as DBResearchUpdate, + dbResult(researchUpdateAfterUpdating.data), [], ); - researchUpdate.research = researchUpdateAfterUpdating.data - .research as unknown as DBResearchItem; + researchUpdate.research = dbResult( + researchUpdateAfterUpdating.data.research, + ); broadcastCoordinationServiceServer.researchUpdate( researchUpdate, @@ -154,7 +156,7 @@ async function updateOrReplaceImage( .single(); if (existingMedia.data && existingMedia.data.images?.length > 0) { - media = (existingMedia.data.images as unknown as DBMedia[]).filter((x) => + media = fromJsonArray(existingMedia.data.images).filter((x) => idsToKeep.includes(x.id), ); } @@ -189,10 +191,10 @@ async function updateOrReplaceFile( let mediaToRemove: MediaFile[] = []; if (existingMedia.data && existingMedia.data.files?.length > 0) { - media = (existingMedia.data.files as unknown as MediaFile[]).filter((x) => + media = fromJsonArray(existingMedia.data.files).filter((x) => idsToKeep.includes(x.id), ); - mediaToRemove = (existingMedia.data.files as unknown as MediaFile[]).filter( + mediaToRemove = fromJsonArray(existingMedia.data.files).filter( (x) => !idsToKeep.includes(x.id), ); } diff --git a/src/routes/api.research.$id.updates.ts b/src/routes/api.research.$id.updates.ts index 80b81313aa..c17d43cf7d 100644 --- a/src/routes/api.research.$id.updates.ts +++ b/src/routes/api.research.$id.updates.ts @@ -10,6 +10,7 @@ import { storageServiceServer } from 'src/services/storageService.server'; import { subscribersServiceServer } from 'src/services/subscribersService.server'; import { updateUserActivity } from 'src/utils/activity.server'; import { validateImages } from 'src/utils/storage'; +import { dbResult, fromJsonArray } from 'src/utils/supabase.types'; export const action = async ({ request, params }: ActionFunctionArgs) => { const { client, headers } = createSupabaseServerClient(request); @@ -36,7 +37,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { .select('id,title,slug,collaborators,author:profiles(id, username)') .eq('id', researchId) .single(); - const research = researchResult.data as unknown as DBResearchItem; + const research = dbResult(researchResult.data); const profileService = new ProfileServiceServer(client); const profile = await profileService.getByAuthId(claims.data.claims.sub); @@ -85,10 +86,10 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const dbResearchUpdate = updateResult.data; const researchUpdate = ResearchUpdate.fromDB( - dbResearchUpdate as unknown as DBResearchUpdate, + dbResult(dbResearchUpdate), [], ); - researchUpdate.research = updateResult.data.research as unknown as DBResearchItem; + researchUpdate.research = dbResult(updateResult.data.research); await uploadAndUpdateImages( uploadedImages, @@ -141,13 +142,13 @@ async function uploadAndUpdateImages( const result = await client .from('research_updates') .update({ - images: mediaResult.media as unknown as Json[], + images: dbResult(mediaResult.media), }) .eq('id', researchUpdate.id) .select(); if (result.data) { - researchUpdate.images = result.data[0].images as unknown as Image[]; + researchUpdate.images = fromJsonArray(result.data[0].images); } } } @@ -166,13 +167,13 @@ async function uploadAndUpdateFiles( const result = await client .from('research_updates') .update({ - files: mediaResult.media as unknown as Json[], + files: dbResult(mediaResult.media), }) .eq('id', researchUpdate.id) .select(); if (result.data) { - researchUpdate.files = result.data[0].files as unknown as MediaFile[]; + researchUpdate.files = fromJsonArray(result.data[0].files); } } } diff --git a/src/routes/api.research.drafts.ts b/src/routes/api.research.drafts.ts index 04de268d1a..25173fd6cd 100644 --- a/src/routes/api.research.drafts.ts +++ b/src/routes/api.research.drafts.ts @@ -5,6 +5,7 @@ import { IMAGE_SIZES } from 'src/config/imageTransforms'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; import { storageServiceServer } from 'src/services/storageService.server'; +import { dbResultArray } from 'src/utils/supabase.types'; export async function loader({ request }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -88,7 +89,7 @@ export async function loader({ request }: LoaderFunctionArgs) { return Response.json({ items: [] }, { headers }); } - const drafts = result.data as unknown as DBResearchItem[]; + const drafts = dbResultArray(result.data); const items = drafts.map((x) => { const images = x.image ? storageServiceServer.getPublicUrls(client, [x.image], IMAGE_SIZES.LIST) diff --git a/src/routes/api.research.ts b/src/routes/api.research.ts index 464657d068..f7ecd69147 100644 --- a/src/routes/api.research.ts +++ b/src/routes/api.research.ts @@ -2,7 +2,6 @@ import type { DBResearchItem, Image, ResearchStatus } from 'oa-shared'; import { ResearchItem } from 'oa-shared'; import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'; import { IMAGE_SIZES } from 'src/config/imageTransforms'; -import type { Json } from 'src/database.types'; import { ITEMS_PER_PAGE } from 'src/pages/Research/constants'; import type { ResearchSortOption } from 'src/pages/Research/ResearchSortOptions.ts'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; @@ -12,6 +11,7 @@ import { storageServiceServer } from 'src/services/storageService.server'; import { subscribersServiceServer } from 'src/services/subscribersService.server'; import { updateUserActivity } from 'src/utils/activity.server'; import { convertToSlug } from 'src/utils/slug'; +import { dbResult, dbResultArray, fromJson, toJson } from 'src/utils/supabase.types'; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); @@ -45,7 +45,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { return Response.json({}, { status: 500, headers }); } - const dbItems = data as unknown as DBResearchItem[]; + const dbItems = dbResultArray(data); if (!dbItems || dbItems.length === 0) { return Response.json({ items: [], total: 0 }, { headers }); @@ -139,7 +139,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { description: data.description, slug, category: data.category ? Number(data.category) : null, - tags: data.tags as unknown as string[], + tags: dbResult(data.tags), collaborators: data.collaborators, status: researchStatus, is_draft: data.isDraft, @@ -153,7 +153,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { } const research = ResearchItem.fromDB( - researchResult.data as unknown as DBResearchItem, + dbResult(researchResult.data), [], [], [], @@ -175,12 +175,12 @@ export const action = async ({ request }: ActionFunctionArgs) => { if (mediaResult?.media && mediaResult.media.length > 0) { const result = await client .from('research') - .update({ image: mediaResult.media[0] as unknown as Json }) + .update({ image: toJson(mediaResult.media[0]) }) .eq('id', research.id) .select(); if (result.data) { - research.image = result.data[0].image as unknown as Image | null; + research.image = fromJson(result.data[0].image); } } } diff --git a/src/routes/api.settings.impact.ts b/src/routes/api.settings.impact.ts index df89ddc76f..e1d36b1848 100644 --- a/src/routes/api.settings.impact.ts +++ b/src/routes/api.settings.impact.ts @@ -4,6 +4,7 @@ import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ImpactServiceServer } from 'src/services/impactService.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; import { updateUserActivity } from 'src/utils/activity.server'; +import { dbResult } from 'src/utils/supabase.types'; export const action = async ({ request }: ActionFunctionArgs) => { const { client, headers } = createSupabaseServerClient(request); @@ -43,7 +44,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { return Response.json({}, { headers, status: 500, statusText: 'Error saving impact' }); } - const impact = result.data as unknown as IUserImpact; + const impact = dbResult(result.data); updateUserActivity(client, claims.data.claims.sub); diff --git a/src/routes/api.settings.map.ts b/src/routes/api.settings.map.ts index 03c79bde44..6e9b3cb32c 100644 --- a/src/routes/api.settings.map.ts +++ b/src/routes/api.settings.map.ts @@ -7,6 +7,7 @@ import { MapPinsServiceServer } from 'src/services/mapPinsService.server'; import { MapServiceServer } from 'src/services/mapService.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; import { updateUserActivity } from 'src/utils/activity.server'; +import { dbResult } from 'src/utils/supabase.types'; export const action = async ({ request }: ActionFunctionArgs) => { const { client, headers } = createSupabaseServerClient(request); @@ -58,7 +59,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { } const pinFactory = new MapPinFactory(client); - const mapPin = pinFactory.fromDBWithProfile(result.data as unknown as DBMapPin); + const mapPin = pinFactory.fromDBWithProfile(dbResult(result.data)); updateUserActivity(client, claims.data.claims.sub); diff --git a/src/routes/api.useful.$contentType.$contentId.users.ts b/src/routes/api.useful.$contentType.$contentId.users.ts index 22f44b561c..4731058ac6 100644 --- a/src/routes/api.useful.$contentType.$contentId.users.ts +++ b/src/routes/api.useful.$contentType.$contentId.users.ts @@ -3,6 +3,7 @@ import type { LoaderFunctionArgs } from 'react-router'; import type { Database } from 'src/database.types'; import { ProfileFactory } from 'src/factories/profileFactory.server'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; +import { dbResult } from 'src/utils/supabase.types'; export async function loader({ request, params }: LoaderFunctionArgs) { const { client, headers } = createSupabaseServerClient(request); @@ -51,7 +52,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { continue; } - const profile = profileFactory.fromDB(row.profiles as unknown as DBProfile); + const profile = profileFactory.fromDB(dbResult(row.profiles)); users.push({ id: profile.id, diff --git a/src/test/factories/ResearchItem.ts b/src/test/factories/ResearchItem.ts index 8f5336c33b..0d36d8ae7a 100644 --- a/src/test/factories/ResearchItem.ts +++ b/src/test/factories/ResearchItem.ts @@ -35,7 +35,7 @@ export const FactoryDBResearchItemUpdate = ( ): DBResearchUpdate => ({ created_by: faker.number.int(100), comment_count: 0, - created_at: faker.date.past(), + created_at: faker.date.past().toISOString(), deleted: false, description: faker.lorem.sentences(2), id: faker.number.int(), @@ -43,7 +43,7 @@ export const FactoryDBResearchItemUpdate = ( files: [], file_download_count: faker.number.int(), file_link: '', - modified_at: faker.date.past(), + modified_at: faker.date.past().toISOString(), research_id: faker.number.int(100), title: faker.lorem.words(), video_url: null, @@ -93,8 +93,8 @@ export const FactoryDBResearchItem = ( id: faker.number.int(), is_draft: false, created_by: faker.number.int(), - modified_at: faker.date.past(), - created_at: faker.date.past(), + modified_at: faker.date.past().toISOString(), + created_at: faker.date.past().toISOString(), deleted: faker.datatype.boolean(), description: faker.lorem.paragraphs(), title: faker.lorem.words(), diff --git a/src/test/factories/dbNotification.ts b/src/test/factories/dbNotification.ts index 52b1674731..fadd4b1689 100644 --- a/src/test/factories/dbNotification.ts +++ b/src/test/factories/dbNotification.ts @@ -8,7 +8,7 @@ export const factoryDBNotification = ( action_type: 'newComment', content_id: 1, content_type: 'comments', - created_at: new Date('2024-01-01T00:00:00Z'), + created_at: '2024-01-01T00:00:00Z', is_read: false, modified_at: null, owned_by: {} as any, diff --git a/src/test/factories/profile.ts b/src/test/factories/profile.ts index 6fb7c1c49c..c5c3f8de74 100644 --- a/src/test/factories/profile.ts +++ b/src/test/factories/profile.ts @@ -16,7 +16,7 @@ export const FactoryDBProfile = (dbProfileOverloads: Partial = {}): D about: '', auth_id: '', cover_images: null, - created_at: faker.date.past(), + created_at: faker.date.past().toISOString(), impact: null, type: { id: faker.number.int(), @@ -33,7 +33,7 @@ export const FactoryDBProfile = (dbProfileOverloads: Partial = {}): D visitor_policy: null, is_blocked_from_messaging: null, is_contactable: false, - last_active: faker.date.past(), + last_active: faker.date.past().toISOString(), website: faker.internet.url(), total_views: 0, profile_type: faker.number.int(), diff --git a/src/utils/supabase.types.ts b/src/utils/supabase.types.ts new file mode 100644 index 0000000000..8638fb5b01 --- /dev/null +++ b/src/utils/supabase.types.ts @@ -0,0 +1,23 @@ +import type { Json } from 'src/database.types'; + +export function toJson(value: T): Json { + return value as unknown as Json; +} + +export function fromJson(value: Json | null | undefined): T | null { + if (value === null || value === undefined) return null; + return value as unknown as T; +} + +export function fromJsonArray(value: Json[] | Json | null | undefined): T[] { + if (!value || !Array.isArray(value)) return []; + return value as unknown as T[]; +} + +export function dbResult(data: unknown): T { + return data as T; +} + +export function dbResultArray(data: unknown): T[] { + return (data ?? []) as T[]; +} From e80a7404085b2daf84c783646ec13eb05c2d3324 Mon Sep 17 00:00:00 2001 From: Georgios Metaxakis Date: Fri, 6 Mar 2026 15:02:41 +0200 Subject: [PATCH 4/4] refactor: convert DB model classes to interfaces, move types to shared - Move database.types.ts from src/ to shared/ (enables DB models to derive from generated types in the future) - Export Database, Tables, Json types from oa-shared package - Convert all DB model classes to interfaces (DBProfile, DBNews, DBComment, DBQuestion, DBResearchItem, DBResearchUpdate, DBProject, DBProjectStep, DBCategory, DBTag, DBProfileTag, DBProfileBadge, DBProfileType, DBMedia, DBNotification, DBMapPin, DBBanner) - Remove fake Object.assign constructors from DB types - Replace new DB*() instantiation patterns with plain object literals - Update all imports to use oa-shared instead of src/database.types - Update db:types script target path --- package.json | 2 +- {src => shared}/database.types.ts | 0 shared/index.ts | 1 + shared/models/banner.ts | 8 +--- shared/models/category.ts | 19 +--------- shared/models/comment.ts | 8 +--- shared/models/library.ts | 14 ++----- shared/models/media.ts | 6 +-- shared/models/news.ts | 4 +- shared/models/profile.ts | 18 +++------ shared/models/profileBadge.ts | 8 +--- shared/models/profileTag.ts | 8 +--- shared/models/profileType.ts | 6 +-- shared/models/question.ts | 8 +--- shared/models/research.ts | 16 ++------ shared/models/tag.ts | 18 +-------- src/repository/supabase.server.ts | 2 +- ...cussions.$sourceType.$sourceId.comments.ts | 8 ++-- src/routes/api.projects.$id.ts | 3 +- src/routes/api.projects.ts | 3 +- src/routes/api.questions.$id.ts | 3 +- src/routes/api.questions.ts | 3 +- .../api.research.$id.updates.$updateId.ts | 3 +- src/routes/api.research.$id.updates.ts | 3 +- .../api.useful.$contentType.$contentId.ts | 2 +- ...pi.useful.$contentType.$contentId.users.ts | 3 +- ...pi.useful.$contentType.$contentId.voted.ts | 2 +- .../api.useful.$contentType.$id.count.ts | 3 +- .../notificationsSupabaseService.server.ts | 37 +++++++++---------- src/services/questionService.ts | 6 +-- src/utils/supabase.types.ts | 2 +- 31 files changed, 68 insertions(+), 159 deletions(-) rename {src => shared}/database.types.ts (100%) diff --git a/package.json b/package.json index 91ae6c43ac..79b566f8bc 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "storybook": "bun run --filter oa-components start", "storybook:build": "bun run build:shared && bun run build:themes && bun run --filter oa-components build:sb", "docs": "bun run --filter oa-docs start", - "db:types": "supabase gen types --local > src/database.types.ts", + "db:types": "supabase gen types --local > shared/database.types.ts", "db:reset": "npx supabase db reset && bun run db:seed", "db:seed": "npx @snaplet/seed sync && npx tsx seed.ts > seed.sql", "commit": "git-cz", diff --git a/src/database.types.ts b/shared/database.types.ts similarity index 100% rename from src/database.types.ts rename to shared/database.types.ts diff --git a/shared/index.ts b/shared/index.ts index 9cdbbbd349..0c553dba36 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -1,3 +1,4 @@ +export * from './database.types'; export * from './messages'; export * from './models'; export * from './utils'; diff --git a/shared/models/banner.ts b/shared/models/banner.ts index 6a63aa4607..b423df27f2 100644 --- a/shared/models/banner.ts +++ b/shared/models/banner.ts @@ -1,16 +1,12 @@ -import type { IDBDocSB, IDoc } from './document'; +import type { IDoc } from './document'; -export class DBBanner implements IDBDocSB { +export interface DBBanner { id: number; created_at: string; modified_at: string | null; text: string; url: string; - - constructor(obj: any) { - Object.assign(this, obj); - } } export class Banner implements IDoc { diff --git a/shared/models/category.ts b/shared/models/category.ts index af3efd4afb..82bb726f3d 100644 --- a/shared/models/category.ts +++ b/shared/models/category.ts @@ -1,28 +1,13 @@ import type { ContentType } from './common'; -import type { IDBDocSB, IDoc } from './document'; +import type { IDoc } from './document'; -export class DBCategory implements IDBDocSB { +export interface DBCategory { id: number; created_at: string; modified_at: string | null; name: string; type: ContentType; - - constructor(obj: any) { - Object.assign(this, obj); - } - - static toDB(category: Category) { - const { createdAt, id, modifiedAt, name, type } = category; - return new DBCategory({ - id, - created_at: new Date(createdAt), - modified_at: modifiedAt ? new Date(modifiedAt) : null, - name, - type, - }); - } } export class Category implements IDoc { diff --git a/shared/models/comment.ts b/shared/models/comment.ts index 3f37f7a22a..e351d3f52d 100644 --- a/shared/models/comment.ts +++ b/shared/models/comment.ts @@ -1,9 +1,9 @@ import type { DBAuthor } from './author'; import { Author } from './author'; import type { DiscussionContentType } from './common'; -import type { IDBDocSB, IDoc } from './document'; +import type { IDoc } from './document'; -export class DBComment implements IDBDocSB { +export interface DBComment { readonly id: number; readonly created_at: string; readonly modified_at: string | null; @@ -18,10 +18,6 @@ export class DBComment implements IDBDocSB { readonly parent_id: number | null; readonly vote_count?: number; readonly has_voted?: boolean; - - constructor(comment: DBComment) { - Object.assign(this, comment); - } } export class Comment implements IDoc { diff --git a/shared/models/library.ts b/shared/models/library.ts index 52aeb5e7ba..86b9580521 100644 --- a/shared/models/library.ts +++ b/shared/models/library.ts @@ -3,7 +3,7 @@ import { Author } from './author'; import type { DBCategory } from './category'; import { Category } from './category'; import type { IConvertedFileMeta } from './common'; -import type { IContentDoc, IDBContentDoc } from './content'; +import type { IContentDoc } from './content'; import type { IDBDownloadable, IDownloadable } from './document'; import type { IFilesForm } from './filesForm'; import type { IImageForm } from './imageForm'; @@ -20,7 +20,7 @@ export const DifficultyLevelRecord: Record = { 'very-hard': 'Very Hard', }; -export class DBProject implements IDBContentDoc, IDBDownloadable, IDBModeration { +export interface DBProject extends IDBDownloadable, IDBModeration { readonly id: number; readonly created_at: string; readonly deleted: boolean | null; @@ -50,10 +50,6 @@ export class DBProject implements IDBContentDoc, IDBDownloadable, IDBModeration file_download_count?: number; moderation: Moderation; moderation_feedback: string; - - constructor(obj: Omit) { - Object.assign(this, obj); - } } export class Project implements IContentDoc, IDownloadable, IModeration { @@ -125,7 +121,7 @@ export class Project implements IContentDoc, IDownloadable, IModeration { } } -export class DBProjectStep { +export interface DBProjectStep { readonly id: number; readonly project_id: number; title: string; @@ -133,10 +129,6 @@ export class DBProjectStep { images: DBMedia[] | null; video_url: string | null; order: number; - - constructor(obj: Omit) { - Object.assign(this, obj); - } } export class ProjectStep { diff --git a/shared/models/media.ts b/shared/models/media.ts index 492b96faea..74c7fe7c2d 100644 --- a/shared/models/media.ts +++ b/shared/models/media.ts @@ -1,11 +1,7 @@ -export class DBMedia { +export interface DBMedia { id: string; path: string; fullPath: string; - - constructor(obj: DBMedia) { - Object.assign(this, obj); - } } interface IMedia { diff --git a/shared/models/news.ts b/shared/models/news.ts index 2bc2f53c10..9fe2a97305 100644 --- a/shared/models/news.ts +++ b/shared/models/news.ts @@ -6,14 +6,14 @@ import { Author } from './author'; import type { DBCategory } from './category'; import { Category } from './category'; import type { IConvertedFileMeta } from './common'; -import type { IContentDoc, IDBContentDoc } from './content'; +import type { IContentDoc } from './content'; import type { DBMedia, Image } from './media'; import type { DBProfileBadge } from './profileBadge'; import { ProfileBadge } from './profileBadge'; import type { SelectValue } from './selectValue'; import type { Tag } from './tag'; -export class DBNews implements IDBContentDoc { +export interface DBNews { readonly id: number; readonly created_at: string; readonly modified_at: string | null; diff --git a/shared/models/profile.ts b/shared/models/profile.ts index 1abe3a6979..ed643f9b6e 100644 --- a/shared/models/profile.ts +++ b/shared/models/profile.ts @@ -1,6 +1,6 @@ import type { Comment } from './comment'; import type { SubscribableContentTypes } from './common'; -import type { IDBDocSB, IDoc } from './document'; +import type { IDoc } from './document'; import type { DBMedia, Image } from './media'; import type { IDBModeration, IModeration, Moderation } from './moderation'; import type { News } from './news'; @@ -15,7 +15,7 @@ import type { Question } from './question'; import type { ResearchUpdate } from './research'; import type { IUserImpact, UserVisitorPreference } from './user'; -export class DBProfile { +export interface DBProfile { readonly id: number; readonly created_at: string; readonly tags?: DBProfileTagJoin[]; @@ -40,10 +40,6 @@ export class DBProfile { auth_id: string; profile_type: number; donations_enabled: boolean; - - constructor(obj: DBProfile) { - Object.assign(this, obj); - } } export class Profile { @@ -131,7 +127,7 @@ export type ProfileListItem = Pick< type NotificationContent = News | Comment | Question | ResearchUpdate; type NotificationSourceContentType = SubscribableContentTypes; -export class DBNotification implements IDBDocSB { +export interface DBNotification { readonly id: number; readonly title: string; readonly action_type: NotificationActionType; @@ -147,10 +143,6 @@ export class DBNotification implements IDBDocSB { readonly triggered_by: DBProfile; readonly triggered_by_id: number; readonly tenant_id: string; - - constructor(obj: Partial) { - Object.assign(this, obj); - } } export class Notification implements IDoc { @@ -385,11 +377,11 @@ export type ProfileFormData = { visitorPreferenceDetails?: UserVisitorPreference['details']; }; -export class DBMapPin implements IDBModeration { +export interface DBMapPin extends IDBModeration { readonly id: number; readonly profile: DBPinProfile; profile_id: number; - country: string; // check if necessary + country: string; country_code: string; name: string | null; administrative: string | null; diff --git a/shared/models/profileBadge.ts b/shared/models/profileBadge.ts index cb9977f338..f8e657e10d 100644 --- a/shared/models/profileBadge.ts +++ b/shared/models/profileBadge.ts @@ -2,20 +2,16 @@ export enum PremiumTier { ONE = 1, } -export class DBProfileBadge { +export interface DBProfileBadge { id: number; name: string; display_name: string; image_url: string; action_url: string | null; premium_tier: number | null; - - constructor(obj: Partial) { - Object.assign(this, obj); - } } -export class DBProfileBadgeJoin { +export interface DBProfileBadgeJoin { profile_badges: DBProfileBadge; } diff --git a/shared/models/profileTag.ts b/shared/models/profileTag.ts index aebd4dc05d..01d3eeb525 100644 --- a/shared/models/profileTag.ts +++ b/shared/models/profileTag.ts @@ -1,17 +1,13 @@ export type ProfileCategory = 'member' | 'space'; -export class DBProfileTag { +export interface DBProfileTag { id: number; created_at: string; name: string; profile_type: string; - - constructor(obj: Partial) { - Object.assign(this, obj); - } } -export class DBProfileTagJoin { +export interface DBProfileTagJoin { profile_tags: DBProfileTag; } diff --git a/shared/models/profileType.ts b/shared/models/profileType.ts index 8f856c0403..ae68f061b7 100644 --- a/shared/models/profileType.ts +++ b/shared/models/profileType.ts @@ -1,4 +1,4 @@ -export class DBProfileType { +export interface DBProfileType { id: number; description: string; display_name: string; @@ -8,10 +8,6 @@ export class DBProfileType { name: string; order: number; is_space: boolean; - - constructor(obj: Partial) { - Object.assign(this, obj); - } } export class ProfileType { diff --git a/shared/models/question.ts b/shared/models/question.ts index 4188cd0fa1..45bfaf0424 100644 --- a/shared/models/question.ts +++ b/shared/models/question.ts @@ -3,12 +3,12 @@ import { Author } from './author'; import type { DBCategory } from './category'; import { Category } from './category'; import type { IConvertedFileMeta } from './common'; -import type { IContentDoc, IDBContentDoc } from './content'; +import type { IContentDoc } from './content'; import type { DBMedia, Image } from './media'; import type { SelectValue } from './selectValue'; import type { Tag } from './tag'; -export class DBQuestion implements IDBContentDoc { +export interface DBQuestion { readonly id: number; is_draft: boolean; readonly created_at: string; @@ -29,10 +29,6 @@ export class DBQuestion implements IDBContentDoc { readonly description: string; readonly images: DBMedia[] | null; - - constructor(question: DBQuestion) { - Object.assign(this, question); - } } export class Question implements IContentDoc { diff --git a/shared/models/research.ts b/shared/models/research.ts index 4fe2bd31bf..a5ff140405 100644 --- a/shared/models/research.ts +++ b/shared/models/research.ts @@ -3,8 +3,8 @@ import { Author } from './author'; import type { DBCategory } from './category'; import { Category } from './category'; import type { IConvertedFileMeta } from './common'; -import type { IContentDoc, IDBContentDoc } from './content'; -import type { IDBDocSB, IDBDownloadable, IDoc, IDownloadable } from './document'; +import type { IContentDoc } from './content'; +import type { IDBDownloadable, IDoc, IDownloadable } from './document'; import type { IFilesForm } from './filesForm'; import type { DBMedia, IMediaFile, Image, MediaFile } from './media'; import type { SelectValue } from './selectValue'; @@ -16,7 +16,7 @@ export const ResearchStatusRecord: Record = { complete: 'Completed', }; -export class DBResearchItem implements IDBContentDoc { +export interface DBResearchItem { readonly id: number; readonly created_at: string; readonly deleted: boolean | null; @@ -41,10 +41,6 @@ export class DBResearchItem implements IDBContentDoc { status: ResearchStatus; is_draft: boolean; collaborators: string[] | null; - - constructor(obj: Omit) { - Object.assign(this, obj); - } } export class ResearchItem implements IContentDoc { @@ -132,7 +128,7 @@ export class ResearchItem implements IContentDoc { } } -export class DBResearchUpdate implements IDBDocSB, IDBDownloadable { +export interface DBResearchUpdate extends IDBDownloadable { readonly id: number; readonly research_id: number; readonly created_at: string; @@ -149,10 +145,6 @@ export class DBResearchUpdate implements IDBDocSB, IDBDownloadable { file_link: string | null; files: IMediaFile[] | null; video_url: string | null; - - constructor(obj: Omit) { - Object.assign(this, obj); - } } export class ResearchUpdate implements IDoc, IDownloadable { diff --git a/shared/models/tag.ts b/shared/models/tag.ts index 4d17fcbb98..baa6c56968 100644 --- a/shared/models/tag.ts +++ b/shared/models/tag.ts @@ -1,25 +1,11 @@ -import type { IDBDocSB, IDoc } from './document'; +import type { IDoc } from './document'; -export class DBTag implements IDBDocSB { +export interface DBTag { id: number; created_at: string; modified_at: string | null; name: string; - - constructor(obj: any) { - Object.assign(this, obj); - } - - static toDB(tag: Tag) { - const { createdAt, id, modifiedAt, name } = tag; - return new DBTag({ - id, - created_at: new Date(createdAt), - modified_at: modifiedAt ? new Date(modifiedAt) : null, - name, - }); - } } export class Tag implements IDoc { diff --git a/src/repository/supabase.server.ts b/src/repository/supabase.server.ts index 450774e9fd..d95e24c8e0 100644 --- a/src/repository/supabase.server.ts +++ b/src/repository/supabase.server.ts @@ -1,5 +1,5 @@ import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'; -import type { Database } from 'src/database.types'; +import type { Database } from 'oa-shared'; // Create a single supabase client for interacting with your database export function createSupabaseServerClient(request: Request) { diff --git a/src/routes/api.discussions.$sourceType.$sourceId.comments.ts b/src/routes/api.discussions.$sourceType.$sourceId.comments.ts index 3bcaf0b725..dd56c29318 100644 --- a/src/routes/api.discussions.$sourceType.$sourceId.comments.ts +++ b/src/routes/api.discussions.$sourceType.$sourceId.comments.ts @@ -1,6 +1,6 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBAuthor, DBProfile, DiscussionContentType } from 'oa-shared'; -import { DBComment, DiscussionContentTypes } from 'oa-shared'; +import type { DBAuthor, DBComment, DBProfile, DiscussionContentType } from 'oa-shared'; +import { DiscussionContentTypes } from 'oa-shared'; import type { LoaderFunctionArgs, Params } from 'react-router'; import { CommentFactory } from 'src/factories/commentFactory.server'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; @@ -137,10 +137,10 @@ export async function action({ params, request }: LoaderFunctionArgs) { notificationsSupabaseServiceServer.createNotificationsNewComment(comment, client, headers); } - const commentDb = new DBComment({ + const commentDb: DBComment = { ...dbResult(commentResult.data), profile: (commentResult.data as any).profiles as DBAuthor, - }); + }; const commentFactory = new CommentFactory(new ImageServiceServer(client)); const comment = await commentFactory.fromDBWithAuthor(commentDb); diff --git a/src/routes/api.projects.$id.ts b/src/routes/api.projects.$id.ts index a189926190..a9eb77b191 100644 --- a/src/routes/api.projects.$id.ts +++ b/src/routes/api.projects.$id.ts @@ -1,8 +1,7 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBMedia, DBProfile, DBProject, Image, MediaFile } from 'oa-shared'; +import type { DBMedia, DBProfile, DBProject, Image, Json, MediaFile } from 'oa-shared'; import { Project, ProjectStep, UserRole } from 'oa-shared'; import type { ActionFunctionArgs } from 'react-router'; -import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { contentServiceServer } from 'src/services/contentService.server'; import { libraryServiceServer } from 'src/services/libraryService.server'; diff --git a/src/routes/api.projects.ts b/src/routes/api.projects.ts index f1ae902ed4..dd1ab2b17f 100644 --- a/src/routes/api.projects.ts +++ b/src/routes/api.projects.ts @@ -1,9 +1,8 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBProfile, DBProject, DBProjectStep, MediaFile, Moderation } from 'oa-shared'; +import type { DBProfile, DBProject, DBProjectStep, Json, MediaFile, Moderation } from 'oa-shared'; import { Project, ProjectStep, UserRole } from 'oa-shared'; import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'; import { IMAGE_SIZES } from 'src/config/imageTransforms'; -import type { Json } from 'src/database.types'; import type { LibrarySortOption } from 'src/pages/Library/Content/List/LibrarySortOptions'; import { ITEMS_PER_PAGE } from 'src/pages/Library/constants'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; diff --git a/src/routes/api.questions.$id.ts b/src/routes/api.questions.$id.ts index d8af65a0c1..5d77e66abb 100644 --- a/src/routes/api.questions.$id.ts +++ b/src/routes/api.questions.$id.ts @@ -1,9 +1,8 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBMedia, DBQuestion } from 'oa-shared'; +import type { DBMedia, DBQuestion, Json } from 'oa-shared'; import { Question } from 'oa-shared'; import type { LoaderFunctionArgs, Params } from 'react-router'; import { IMAGE_SIZES } from 'src/config/imageTransforms'; -import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; import { questionServiceServer } from 'src/services/questionService.server'; diff --git a/src/routes/api.questions.ts b/src/routes/api.questions.ts index c02511eebb..1768cbfa5d 100644 --- a/src/routes/api.questions.ts +++ b/src/routes/api.questions.ts @@ -1,9 +1,8 @@ // TODO: split this in separate files once we update remix to NOT use file-based routing -import type { DBProfile, DBQuestion, Image, Moderation } from 'oa-shared'; +import type { DBProfile, DBQuestion, Image, Json, Moderation } from 'oa-shared'; import { Question } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; -import type { Json } from 'src/database.types'; import { ITEMS_PER_PAGE } from 'src/pages/Question/constants'; import type { QuestionSortOption } from 'src/pages/Question/QuestionSortOptions'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; diff --git a/src/routes/api.research.$id.updates.$updateId.ts b/src/routes/api.research.$id.updates.$updateId.ts index a01d5d1825..de25bcad5e 100644 --- a/src/routes/api.research.$id.updates.$updateId.ts +++ b/src/routes/api.research.$id.updates.$updateId.ts @@ -1,8 +1,7 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBMedia, DBResearchItem, DBResearchUpdate, MediaFile } from 'oa-shared'; +import type { DBMedia, DBResearchItem, DBResearchUpdate, Json, MediaFile } from 'oa-shared'; import { ResearchUpdate } from 'oa-shared'; import type { ActionFunctionArgs } from 'react-router'; -import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { broadcastCoordinationServiceServer } from 'src/services/broadcastCoordinationService.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; diff --git a/src/routes/api.research.$id.updates.ts b/src/routes/api.research.$id.updates.ts index c17d43cf7d..2bd6854da1 100644 --- a/src/routes/api.research.$id.updates.ts +++ b/src/routes/api.research.$id.updates.ts @@ -1,8 +1,7 @@ import type { SupabaseClient } from '@supabase/supabase-js'; -import type { DBProfile, DBResearchItem, DBResearchUpdate, Image, MediaFile } from 'oa-shared'; +import type { DBProfile, DBResearchItem, DBResearchUpdate, Image, Json, MediaFile } from 'oa-shared'; import { ResearchUpdate, UserRole } from 'oa-shared'; import type { ActionFunctionArgs } from 'react-router'; -import type { Json } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { broadcastCoordinationServiceServer } from 'src/services/broadcastCoordinationService.server'; import { ProfileServiceServer } from 'src/services/profileService.server'; diff --git a/src/routes/api.useful.$contentType.$contentId.ts b/src/routes/api.useful.$contentType.$contentId.ts index a0200391ee..aba56c6fa2 100644 --- a/src/routes/api.useful.$contentType.$contentId.ts +++ b/src/routes/api.useful.$contentType.$contentId.ts @@ -1,5 +1,5 @@ import type { LoaderFunctionArgs } from 'react-router'; -import type { Database } from 'src/database.types'; +import type { Database } from 'oa-shared'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { updateUserActivity } from 'src/utils/activity.server'; diff --git a/src/routes/api.useful.$contentType.$contentId.users.ts b/src/routes/api.useful.$contentType.$contentId.users.ts index 4731058ac6..1bc288099f 100644 --- a/src/routes/api.useful.$contentType.$contentId.users.ts +++ b/src/routes/api.useful.$contentType.$contentId.users.ts @@ -1,6 +1,5 @@ -import type { DBProfile, ProfileListItem } from 'oa-shared'; +import type { Database, DBProfile, ProfileListItem } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; -import type { Database } from 'src/database.types'; import { ProfileFactory } from 'src/factories/profileFactory.server'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; import { dbResult } from 'src/utils/supabase.types'; diff --git a/src/routes/api.useful.$contentType.$contentId.voted.ts b/src/routes/api.useful.$contentType.$contentId.voted.ts index 30f152ec55..af2175b030 100644 --- a/src/routes/api.useful.$contentType.$contentId.voted.ts +++ b/src/routes/api.useful.$contentType.$contentId.voted.ts @@ -1,5 +1,5 @@ import type { LoaderFunctionArgs } from 'react-router'; -import type { Database } from 'src/database.types'; +import type { Database } from 'oa-shared'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; export async function loader({ request, params }: LoaderFunctionArgs) { diff --git a/src/routes/api.useful.$contentType.$id.count.ts b/src/routes/api.useful.$contentType.$id.count.ts index 4f882422e4..ad02326a91 100644 --- a/src/routes/api.useful.$contentType.$id.count.ts +++ b/src/routes/api.useful.$contentType.$id.count.ts @@ -1,6 +1,5 @@ -import type { UsefulContentType } from 'oa-shared'; +import type { Database, UsefulContentType } from 'oa-shared'; import type { LoaderFunctionArgs } from 'react-router'; -import type { Database } from 'src/database.types'; import { createSupabaseServerClient } from 'src/repository/supabase.server'; export async function loader({ request, params }: LoaderFunctionArgs) { diff --git a/src/services/notificationsSupabaseService.server.ts b/src/services/notificationsSupabaseService.server.ts index 11d2fb9665..cbd7246e1e 100644 --- a/src/services/notificationsSupabaseService.server.ts +++ b/src/services/notificationsSupabaseService.server.ts @@ -1,13 +1,13 @@ import type { SupabaseClient } from '@supabase/supabase-js'; import type { DBComment, + DBNotification, DBProfile, DBResearchItem, ResearchUpdate, SubscribableContentTypes, SubscribedUser, } from 'oa-shared'; -import { DBNotification } from 'oa-shared'; import { notificationEmailService } from './notificationEmailService.server'; const getSubscribedUsers = async ( @@ -38,21 +38,18 @@ const createNotifications = async ( subscriberIds: SubscribedUser[], ): Promise => { try { - const notificationsToInsert = subscriberIds.map( - (subscriber) => - new DBNotification({ - action_type: notification.action_type, - content_id: notification.content_id, - content_type: notification.content_type, - source_content_id: notification.source_content_id, - source_content_type: notification.source_content_type, - title: notification.title, - triggered_by_id: notification.triggered_by_id, - owned_by_id: subscriber.profile_id, - is_read: false, - tenant_id: process.env.TENANT_ID!, - }), - ); + const notificationsToInsert = subscriberIds.map((subscriber) => ({ + action_type: notification.action_type, + content_id: notification.content_id, + content_type: notification.content_type, + source_content_id: notification.source_content_id, + source_content_type: notification.source_content_type, + title: notification.title, + triggered_by_id: notification.triggered_by_id, + owned_by_id: subscriber.profile_id, + is_read: false, + tenant_id: process.env.TENANT_ID!, + })); const response = await client.from('notifications').insert(notificationsToInsert); @@ -98,14 +95,14 @@ const createNotificationsNewComment = async ( (user) => user.profile_id !== comment.created_by, ); - const notification = new DBNotification({ + const notification = { action_type: isReply ? 'newReply' : 'newComment', content_id: comment.id!, title: title, triggered_by_id: comment.created_by!, triggered_by: (comment as any).profiles as DBProfile, content_type: 'comments', - }); + } as DBNotification; await createNotifications(client, notification, subscribers); @@ -139,14 +136,14 @@ const createNotificationsResearchUpdate = async ( try { const contentType: SubscribableContentTypes = 'research'; const subscribers = await getSubscribedUsers(research.id, contentType, client); - const notification = new DBNotification({ + const notification = { action_type: 'newContent', title: research.title, content_id: researchUpdate.id!, content_type: 'research_updates', triggered_by_id: profile.id, triggered_by: profile, - }); + } as DBNotification; await createNotifications(client, notification, subscribers); diff --git a/src/services/questionService.ts b/src/services/questionService.ts index a52fc24900..5be24a5579 100644 --- a/src/services/questionService.ts +++ b/src/services/questionService.ts @@ -1,5 +1,5 @@ -import type { QuestionFormData } from 'oa-shared'; -import { DBQuestion, Question } from 'oa-shared'; +import type { DBQuestion, QuestionFormData } from 'oa-shared'; +import { Question } from 'oa-shared'; import { getCleanFileName } from 'src/utils/storage'; const upsert = async (id: number | null, question: QuestionFormData) => { @@ -51,7 +51,7 @@ const upsert = async (id: number | null, question: QuestionFormData) => { const newQuestion = await response.json(); - return Question.fromDB(new DBQuestion(newQuestion.question), []); + return Question.fromDB(newQuestion.question as DBQuestion, []); }; export const questionService = { diff --git a/src/utils/supabase.types.ts b/src/utils/supabase.types.ts index 8638fb5b01..aaef813565 100644 --- a/src/utils/supabase.types.ts +++ b/src/utils/supabase.types.ts @@ -1,4 +1,4 @@ -import type { Json } from 'src/database.types'; +import type { Json } from 'oa-shared'; export function toJson(value: T): Json { return value as unknown as Json;