Skip to main content

Overview

The query functions provide a type-safe interface for database operations. All functions are located in src/lib/supabase/queries.ts and use the Supabase client for data access.
import { createClient } from '@/lib/supabase/client';
import type { AnimeEntry, Review, CustomList, Comment, ReviewVote } from '@/types/database';

const supabase = createClient();

Anime Entries

Functions for managing user’s anime list entries.

getAnimeEntries()

Retrieves all anime entries for a specific user, ordered by creation date (newest first).
userId
string
required
The user’s UUID from Supabase Auth
Returns: Promise<AnimeEntry[]> Throws: Supabase error if query fails
const entries = await getAnimeEntries(user.id);
// Returns array of anime entries

getAnimeEntry()

Retrieves a specific anime entry for a user.
userId
string
required
The user’s UUID
animeId
number
required
The anime ID from external API (e.g., MyAnimeList)
Returns: Promise<AnimeEntry | null> Throws: Supabase error if query fails (except PGRST116 - not found)
const entry = await getAnimeEntry(user.id, 1535);
if (entry) {
  console.log(`Status: ${entry.status}`);
}

upsertAnimeEntry()

Creates or updates an anime entry. Uses (user_id, anime_id) as conflict resolution key.
entry
Omit<AnimeEntry, 'id' | 'created_at' | 'updated_at'>
required
Anime entry data without auto-generated fields
Required fields in entry:
  • user_id - User UUID
  • anime_id - Anime ID from external API
  • title - Anime title
  • status - One of: ‘watching’, ‘completed’, ‘on-hold’, ‘dropped’, ‘plan-to-watch’
  • episodes_watched - Number of episodes watched (default: 0)
Returns: Promise<AnimeEntry> Throws: Supabase error if operation fails
const newEntry = await upsertAnimeEntry({
  user_id: user.id,
  anime_id: 1535,
  title: 'Death Note',
  title_english: 'Death Note',
  status: 'watching',
  episodes_watched: 5,
  score: 9,
  tags: ['psychological', 'thriller'],
  favorite: true,
  rewatch_count: 0,
  genres: ['Mystery', 'Thriller'],
  episodes: 37,
  type: 'TV'
});
The function automatically sets updated_at to the current timestamp.

deleteAnimeEntry()

Deletes an anime entry for a specific user.
userId
string
required
The user’s UUID
animeId
number
required
The anime ID to delete
Returns: Promise<void> Throws: Supabase error if deletion fails
await deleteAnimeEntry(user.id, 1535);

Reviews

Functions for managing anime reviews.

getReviews()

Retrieves reviews with flexible filtering options, ordered by update date (newest first).
animeId
number
Filter by specific anime ID
userId
string
Filter by specific user ID
status
'draft' | 'published'
Filter by publication status
limit
number
Maximum number of reviews to return
Returns: Promise<Review[]> Throws: Supabase error if query fails
// Get all published reviews for an anime
const reviews = await getReviews(1535, undefined, 'published');

// Get user's draft reviews
const drafts = await getReviews(undefined, user.id, 'draft');

// Get recent published reviews (limited)
const recent = await getReviews(undefined, undefined, 'published', 10);
The function includes console logging for performance monitoring.

getReview()

Retrieves a single review by ID.
reviewId
string
required
The review UUID
Returns: Promise<Review> Throws: Supabase error if query fails or review not found
const review = await getReview('550e8400-e29b-41d4-a716-446655440000');

getUserReview()

Retrieves a user’s review for a specific anime.
userId
string
required
The user’s UUID
animeId
number
required
The anime ID
Returns: Promise<Review | null> Throws: Supabase error if query fails (except PGRST116 - not found)
const userReview = await getUserReview(user.id, 1535);
if (userReview) {
  console.log(`Rating: ${userReview.rating}/10`);
}

upsertReview()

Creates or updates a review with complex logic for handling drafts and published reviews.
review
Omit<Review, 'created_at' | 'updated_at' | 'helpful_votes'> & { id?: string }
required
Review data (id optional for creation)
Required fields:
  • user_id - User UUID
  • anime_id - Anime ID
  • rating - Overall rating (1-10)
  • title - Review title
  • body - Review content
  • spoilers - Whether contains spoilers
  • watch_status - One of: ‘completed’, ‘watching’, ‘dropped’, ‘plan-to-watch’
  • status - ‘draft’ or ‘published’
  • tags - Array of tags
Returns: Promise<Review> Throws: Supabase error if operation fails
// Create new draft review
const draft = await upsertReview({
  user_id: user.id,
  anime_id: 1535,
  rating: 9,
  story_rating: 10,
  animation_rating: 8,
  sound_rating: 9,
  character_rating: 9,
  enjoyment_rating: 10,
  title: 'A Psychological Masterpiece',
  body: 'Death Note is an exceptional thriller...',
  spoilers: false,
  watch_status: 'completed',
  episodes_watched: 37,
  tags: ['psychological', 'must-watch'],
  pros: 'Brilliant mind games and character development',
  cons: 'Second half slightly weaker',
  recommendation: 'highly-recommend',
  status: 'draft'
});

// Publish existing review
const published = await upsertReview({
  ...draft,
  id: draft.id,
  status: 'published'
});
Publishing a review when another published review exists for the same user/anime will delete the old one.

deleteReview()

Permanently deletes a review and all associated votes/comments (CASCADE).
reviewId
string
required
The review UUID to delete
Returns: Promise<void> Throws: Supabase error if deletion fails
await deleteReview(review.id);

Review Votes

Functions for helpful/not helpful voting on reviews.

voteReview()

Casts or updates a vote on a review. Uses upsert to allow vote changes.
reviewId
string
required
The review UUID
userId
string
required
The voting user’s UUID
helpful
boolean
required
True for helpful, false for not helpful
Returns: Promise<ReviewVote> Throws: Supabase error if operation fails
// Vote review as helpful
await voteReview(review.id, user.id, true);

// Change vote to not helpful
await voteReview(review.id, user.id, false);
The review’s helpful_votes count is automatically updated by a database trigger.

getUserReviewVote()

Retrieves a user’s vote on a specific review.
reviewId
string
required
The review UUID
userId
string
required
The user’s UUID
Returns: Promise<ReviewVote | null> Throws: Supabase error if query fails (except PGRST116 - not found)
const vote = await getUserReviewVote(review.id, user.id);
if (vote) {
  console.log(vote.helpful ? 'Voted helpful' : 'Voted not helpful');
}

Comments

Functions for managing review comments with threading support.

getComments()

Retrieves all non-deleted comments for a review, ordered chronologically.
reviewId
string
required
The review UUID
Returns: Promise<Comment[]> Throws: Supabase error if query fails
const comments = await getComments(review.id);
// Filter top-level comments
const topLevel = comments.filter(c => !c.parent_id);
// Filter replies to a comment
const replies = comments.filter(c => c.parent_id === comment.id);
Comments with deleted_at !== null are automatically filtered out.

createComment()

Creates a new comment or reply on a review.
comment
Omit<Comment, 'id' | 'created_at' | 'updated_at' | 'deleted_at'>
required
Comment data without auto-generated fields
Required fields:
  • review_id - Review UUID
  • user_id - Commenter’s UUID
  • content - Comment text
  • parent_id - Parent comment UUID (null for top-level comments)
Returns: Promise<Comment> Throws: Supabase error if creation fails
// Create top-level comment
const comment = await createComment({
  review_id: review.id,
  user_id: user.id,
  parent_id: null,
  content: 'Great review! I totally agree about the second half.'
});

// Create reply
const reply = await createComment({
  review_id: review.id,
  user_id: user.id,
  parent_id: comment.id,
  content: 'Thanks for reading!'
});

updateComment()

Updates the content of an existing comment.
commentId
string
required
The comment UUID
content
string
required
New comment text
Returns: Promise<Comment> Throws: Supabase error if update fails
const updated = await updateComment(
  comment.id,
  'Great review! I totally agree about the character development.'
);
The updated_at timestamp is automatically set by a database trigger.

deleteComment()

Soft deletes a comment by setting deleted_at timestamp.
commentId
string
required
The comment UUID to delete
Returns: Promise<void> Throws: Supabase error if deletion fails
await deleteComment(comment.id);
// Comment remains in database but won't appear in queries
This is a soft delete. The comment data remains in the database but won’t be returned by getComments().

Custom Lists

Functions for managing user-created anime lists.

getCustomLists()

Retrieves all custom lists for a user with entry counts.
userId
string
required
The user’s UUID
Returns: Promise<(CustomList & { custom_list_entries: { anime_id: number }[] })[]> Throws: Supabase error if query fails
const lists = await getCustomLists(user.id);
lists.forEach(list => {
  console.log(`${list.name}: ${list.custom_list_entries.length} anime`);
});
The query joins with custom_list_entries to include anime IDs in each list.

createCustomList()

Creates a new custom list.
list
Omit<CustomList, 'id' | 'created_at' | 'updated_at'>
required
List data without auto-generated fields
Required fields:
  • user_id - User UUID
  • name - List name
  • is_public - Public visibility (default: false)
Returns: Promise<CustomList> Throws: Supabase error if creation fails
const list = await createCustomList({
  user_id: user.id,
  name: 'Best Psychological Anime',
  description: 'Mind-bending anime that make you think',
  is_public: true
});

updateCustomList()

Updates properties of a custom list.
listId
string
required
The list UUID
updates
Partial<CustomList>
required
Fields to update
Returns: Promise<CustomList> Throws: Supabase error if update fails
const updated = await updateCustomList(list.id, {
  name: 'Top Psychological Thrillers',
  is_public: true
});

deleteCustomList()

Deletes a custom list and all its entries (CASCADE).
listId
string
required
The list UUID to delete
Returns: Promise<void> Throws: Supabase error if deletion fails
await deleteCustomList(list.id);

addAnimeToList()

Adds an anime to a custom list.
listId
string
required
The list UUID
animeId
number
required
The anime ID to add
Returns: Promise<CustomListEntry> Throws: Supabase error if operation fails (e.g., duplicate entry)
await addAnimeToList(list.id, 1535); // Add Death Note
Adding the same anime twice to a list will fail due to unique constraint.

removeAnimeFromList()

Removes an anime from a custom list.
listId
string
required
The list UUID
animeId
number
required
The anime ID to remove
Returns: Promise<void> Throws: Supabase error if deletion fails
await removeAnimeFromList(list.id, 1535);

Error Handling

All query functions throw Supabase errors on failure. Wrap calls in try-catch blocks:
try {
  const entries = await getAnimeEntries(user.id);
  // Process entries
} catch (error) {
  console.error('Failed to fetch anime entries:', error);
  // Show user-friendly error message
}

Common Error Codes

  • PGRST116 - Row not found (handled by getAnimeEntry, getUserReview, etc.)
  • 23505 - Unique constraint violation
  • 23503 - Foreign key constraint violation
  • 42501 - Insufficient privileges (RLS policy denied access)

Usage with React

import { useEffect, useState } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { getAnimeEntries, upsertAnimeEntry } from '@/lib/supabase/queries';
import type { AnimeEntry } from '@/types/database';

export default function AnimeList() {
  const { user } = useAuth();
  const [entries, setEntries] = useState<AnimeEntry[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (user) {
      loadEntries();
    }
  }, [user]);

  async function loadEntries() {
    try {
      const data = await getAnimeEntries(user.id);
      setEntries(data);
    } catch (error) {
      console.error('Error loading entries:', error);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div>
      {loading ? 'Loading...' : entries.map(entry => (
        <div key={entry.id}>{entry.title}</div>
      ))}
    </div>
  );
}

Build docs developers (and LLMs) love