diff --git a/.prettierignore b/.prettierignore index 853ad72..414419f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -15,6 +15,9 @@ yarn.lock # Ignore scripts folder scripts/ +# Generated Supabase types (kept byte-identical to `supabase gen types` output) +src/lib/types/database.ts + # Ignore Playwright artifacts test-results/ playwright-report/ diff --git a/eslint.config.js b/eslint.config.js index 6f5d5c2..3a36d34 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,6 +10,9 @@ const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); export default ts.config( includeIgnoreFile(gitignorePath), + // Generated Supabase types are kept byte-identical to the `supabase gen types` + // output, so they are exempt from lint/format rules. + { ignores: ['src/lib/types/database.ts'] }, js.configs.recommended, ...ts.configs.recommended, ...svelte.configs.recommended, diff --git a/src/lib/queries/contestQueries.ts b/src/lib/queries/contestQueries.ts index ac98118..92eb254 100644 --- a/src/lib/queries/contestQueries.ts +++ b/src/lib/queries/contestQueries.ts @@ -102,7 +102,9 @@ export async function fetchContestFeedback(): Promise [item.contest_id, item.feedback_type])); + return Object.fromEntries( + data.map((item) => [item.contest_id, item.feedback_type as 'like' | 'dislike' | null]) + ); } catch (error) { console.error('Failed to fetch user contest feedback:', error); return {}; diff --git a/src/lib/queries/problemQueries.ts b/src/lib/queries/problemQueries.ts index e660ad5..b8292f1 100644 --- a/src/lib/queries/problemQueries.ts +++ b/src/lib/queries/problemQueries.ts @@ -102,7 +102,9 @@ export async function fetchProblemFeedback(): Promise [item.problem_id, item.feedback_type])); + return Object.fromEntries( + data.map((item) => [item.problem_id, item.feedback_type as 'like' | 'dislike' | null]) + ); } catch (error) { console.error('Failed to fetch user feedback:', error); return {}; diff --git a/src/lib/server/authorization.ts b/src/lib/server/authorization.ts index d5485cb..f078b7e 100644 --- a/src/lib/server/authorization.ts +++ b/src/lib/server/authorization.ts @@ -1,11 +1,12 @@ import { json } from '@sveltejs/kit'; import { createClient, type SupabaseClient } from '@supabase/supabase-js'; import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'; +import type { Database } from '$lib/types/database'; export type AuthorizedUser = { authorized: true; userId: string; - supabase: SupabaseClient; + supabase: SupabaseClient; }; export type AuthorizationDenied = { @@ -28,7 +29,7 @@ export async function requireUser(request: Request): Promise(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { global: { headers: { Authorization: `Bearer ${token}` } }, auth: { persistSession: false, autoRefreshToken: false } }); diff --git a/src/lib/services/database.ts b/src/lib/services/database.ts index 55d4129..712802f 100644 --- a/src/lib/services/database.ts +++ b/src/lib/services/database.ts @@ -1,7 +1,8 @@ import { createClient } from '@supabase/supabase-js'; import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; +import type { Database } from '$lib/types/database'; const supabaseUrl = PUBLIC_SUPABASE_URL; const supabaseKey = PUBLIC_SUPABASE_ANON_KEY; -export const supabase = createClient(supabaseUrl, supabaseKey); +export const supabase = createClient(supabaseUrl, supabaseKey); diff --git a/src/lib/types/database.ts b/src/lib/types/database.ts new file mode 100644 index 0000000..0e3aaad --- /dev/null +++ b/src/lib/types/database.ts @@ -0,0 +1,506 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] + +export type Database = { + // Allows to automatically instantiate createClient with right options + // instead of createClient(URL, KEY) + __InternalSupabase: { + PostgrestVersion: "12.2.3 (519615d)" + } + public: { + Tables: { + contests: { + Row: { + added_by: string + added_by_url: string + date_added: string | null + difficulty: number | null + dislikes: number | null + duration_seconds: number + id: string + likes: number | null + name: string + type: string | null + url: string + } + Insert: { + added_by: string + added_by_url: string + date_added?: string | null + difficulty?: number | null + dislikes?: number | null + duration_seconds: number + id?: string + likes?: number | null + name: string + type?: string | null + url: string + } + Update: { + added_by?: string + added_by_url?: string + date_added?: string | null + difficulty?: number | null + dislikes?: number | null + duration_seconds?: number + id?: string + likes?: number | null + name?: string + type?: string | null + url?: string + } + Relationships: [] + } + problems: { + Row: { + added_by: string + added_by_url: string + date_added: string + difficulty: number | null + dislikes: number + id: string + likes: number + name: string + solved: number | null + tags: string[] + type: string | null + url: string + } + Insert: { + added_by: string + added_by_url: string + date_added?: string + difficulty?: number | null + dislikes?: number + id?: string + likes?: number + name: string + solved?: number | null + tags: string[] + type?: string | null + url: string + } + Update: { + added_by?: string + added_by_url?: string + date_added?: string + difficulty?: number | null + dislikes?: number + id?: string + likes?: number + name?: string + solved?: number | null + tags?: string[] + type?: string | null + url?: string + } + Relationships: [] + } + user_contest_feedback: { + Row: { + contest_id: string + created_at: string | null + feedback_type: string + id: string + updated_at: string | null + user_id: string + } + Insert: { + contest_id: string + created_at?: string | null + feedback_type: string + id?: string + updated_at?: string | null + user_id: string + } + Update: { + contest_id?: string + created_at?: string | null + feedback_type?: string + id?: string + updated_at?: string | null + user_id?: string + } + Relationships: [ + { + foreignKeyName: "user_contest_feedback_contest_id_fkey" + columns: ["contest_id"] + isOneToOne: false + referencedRelation: "contests" + referencedColumns: ["id"] + }, + ] + } + user_contest_participation: { + Row: { + contest_id: string + id: string + participated_at: string | null + user_id: string + } + Insert: { + contest_id: string + id?: string + participated_at?: string | null + user_id: string + } + Update: { + contest_id?: string + id?: string + participated_at?: string | null + user_id?: string + } + Relationships: [ + { + foreignKeyName: "user_contest_participation_contest_id_fkey" + columns: ["contest_id"] + isOneToOne: false + referencedRelation: "contests" + referencedColumns: ["id"] + }, + ] + } + user_platform_usernames: { + Row: { + codeforces_username: string | null + created_at: string | null + id: string + kattis_username: string | null + updated_at: string | null + user_id: string + } + Insert: { + codeforces_username?: string | null + created_at?: string | null + id?: string + kattis_username?: string | null + updated_at?: string | null + user_id: string + } + Update: { + codeforces_username?: string | null + created_at?: string | null + id?: string + kattis_username?: string | null + updated_at?: string | null + user_id?: string + } + Relationships: [] + } + user_preferences: { + Row: { + created_at: string | null + hide_from_leaderboard: boolean + id: string + theme: string + updated_at: string | null + user_id: string + } + Insert: { + created_at?: string | null + hide_from_leaderboard?: boolean + id?: string + theme?: string + updated_at?: string | null + user_id: string + } + Update: { + created_at?: string | null + hide_from_leaderboard?: boolean + id?: string + theme?: string + updated_at?: string | null + user_id?: string + } + Relationships: [] + } + user_problem_feedback: { + Row: { + created_at: string | null + feedback_type: string + id: string + problem_id: string + updated_at: string | null + user_id: string + } + Insert: { + created_at?: string | null + feedback_type: string + id?: string + problem_id: string + updated_at?: string | null + user_id: string + } + Update: { + created_at?: string | null + feedback_type?: string + id?: string + problem_id?: string + updated_at?: string | null + user_id?: string + } + Relationships: [ + { + foreignKeyName: "user_problem_feedback_problem_id_fkey" + columns: ["problem_id"] + isOneToOne: false + referencedRelation: "problems" + referencedColumns: ["id"] + }, + ] + } + user_roles: { + Row: { + created_at: string | null + id: string + role: string + user_id: string + } + Insert: { + created_at?: string | null + id?: string + role: string + user_id: string + } + Update: { + created_at?: string | null + id?: string + role?: string + user_id?: string + } + Relationships: [] + } + user_solved_problems: { + Row: { + id: string + problem_id: string + solved_at: string | null + user_id: string + } + Insert: { + id?: string + problem_id: string + solved_at?: string | null + user_id: string + } + Update: { + id?: string + problem_id?: string + solved_at?: string | null + user_id?: string + } + Relationships: [ + { + foreignKeyName: "user_solved_problems_problem_id_fkey" + columns: ["problem_id"] + isOneToOne: false + referencedRelation: "problems" + referencedColumns: ["id"] + }, + ] + } + } + Views: { + [_ in never]: never + } + Functions: { + get_leaderboard: { + Args: never + Returns: { + avatar_url: string + earliest_solves_sum: number + github_url: string + problems_solved: number + rank: number + user_id: string + username: string + }[] + } + get_user_solved_problems: { + Args: { p_user_id: string } + Returns: { + problem_id: string + }[] + } + update_contest_feedback: { + Args: { p_contest_id: string; p_is_like: boolean } + Returns: { + added_by: string + added_by_url: string + date_added: string | null + difficulty: number | null + dislikes: number | null + duration_seconds: number + id: string + likes: number | null + name: string + type: string | null + url: string + }[] + SetofOptions: { + from: "*" + to: "contests" + isOneToOne: false + isSetofReturn: true + } + } + update_problem_feedback: { + Args: { p_is_like: boolean; p_problem_id: string } + Returns: { + added_by: string + added_by_url: string + date_added: string + difficulty: number | null + dislikes: number + id: string + likes: number + name: string + solved: number | null + tags: string[] + type: string | null + url: string + }[] + SetofOptions: { + from: "*" + to: "problems" + isOneToOne: false + isSetofReturn: true + } + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} + +type DatabaseWithoutInternals = Omit + +type DefaultSchema = DatabaseWithoutInternals[Extract] + +export type Tables< + DefaultSchemaTableNameOrOptions extends + | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) + | { schema: keyof DatabaseWithoutInternals }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) + : never = never, +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R + } + ? R + : never + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & + DefaultSchema["Views"]) + ? (DefaultSchema["Tables"] & + DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema["Tables"] + | { schema: keyof DatabaseWithoutInternals }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I + } + ? I + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] + ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema["Tables"] + | { schema: keyof DatabaseWithoutInternals }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U + } + ? U + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] + ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + DefaultSchemaEnumNameOrOptions extends + | keyof DefaultSchema["Enums"] + | { schema: keyof DatabaseWithoutInternals }, + EnumName extends DefaultSchemaEnumNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] + : never = never, +> = DefaultSchemaEnumNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] + : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] + ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] + : never + +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof DefaultSchema["CompositeTypes"] + | { schema: keyof DatabaseWithoutInternals }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof DatabaseWithoutInternals + } + ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + : never = never, +> = PublicCompositeTypeNameOrOptions extends { + schema: keyof DatabaseWithoutInternals +} + ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] + ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] + : never + +export const Constants = { + public: { + Enums: {}, + }, +} as const diff --git a/src/routes/api/codeforces/user-solves/+server.ts b/src/routes/api/codeforces/user-solves/+server.ts index d8719aa..b1f285d 100644 --- a/src/routes/api/codeforces/user-solves/+server.ts +++ b/src/routes/api/codeforces/user-solves/+server.ts @@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit'; import { createClient } from '@supabase/supabase-js'; import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; import { env as publicEnv } from '$env/dynamic/public'; +import type { Database } from '$lib/types/database'; import { codeforcesUserStatusUrl, extractSolvedProblemUrls, @@ -124,7 +125,7 @@ export const GET: RequestHandler = async ({ url, request, fetch }) => { // Intersect against tracked problems server-side. Public read under the anon // key; only id/url/name are selected, and the client never supplies these. - const anon = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { + const anon = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { auth: { persistSession: false, autoRefreshToken: false } }); const { data: problems, error: problemsError } = await anon