Overview
All database types are defined in src/types/database.ts and match the PostgreSQL schema exactly. These types provide full type safety for database operations.
import type {
Profile,
AnimeEntry,
Review,
ReviewVote,
Comment,
CommentVote,
CustomList,
CustomListEntry
} from '@/types/database';
Core Types
Profile
User profile information extending Supabase Auth.
interface Profile {
id: string;
username: string;
avatar_url: string | null;
bio: string | null;
created_at: string;
updated_at: string;
}
User UUID from Supabase Auth (primary key)
URL to avatar image in Supabase Storage
ISO 8601 timestamp when profile was created
ISO 8601 timestamp when profile was last updated
Usage:
const profile: Profile = {
id: '550e8400-e29b-41d4-a716-446655440000',
username: 'otaku_master',
avatar_url: 'https://example.com/avatar.jpg',
bio: 'Anime enthusiast since 2010',
created_at: '2024-01-15T10:30:00Z',
updated_at: '2024-03-01T15:45:00Z'
};
AnimeEntry
User’s anime list entry with watch status and metadata.
interface AnimeEntry {
id: string;
user_id: string;
anime_id: number;
title: string;
title_english: string | null;
title_japanese: string | null;
image: string | null;
type: string | null;
episodes: number | null;
status: 'watching' | 'completed' | 'on-hold' | 'dropped' | 'plan-to-watch';
episodes_watched: number;
score: number | null;
start_date: string | null;
finish_date: string | null;
notes: string | null;
tags: string[];
favorite: boolean;
rewatch_count: number;
priority: 'low' | 'medium' | 'high' | null;
genres: string[];
year: number | null;
rating: string | null;
created_at: string;
updated_at: string;
}
Entry UUID (auto-generated)
User UUID (foreign key to profiles)
ID from external anime API (e.g., MyAnimeList)
English title translation
Anime type (TV, Movie, OVA, etc.)
status
'watching' | 'completed' | 'on-hold' | 'dropped' | 'plan-to-watch'
required
Current watch status
Episodes watched so far (default: 0)
ISO 8601 date when started watching
ISO 8601 date when finished watching
Personal notes about the anime
Custom tags array (default: empty)
Whether marked as favorite (default: false)
Number of times rewatched (default: 0)
priority
'low' | 'medium' | 'high' | null
Priority level for plan-to-watch entries
Anime genres array (default: empty)
Content rating (G, PG-13, R, etc.)
ISO 8601 timestamp when entry was created
ISO 8601 timestamp when entry was last updated
Usage:
const entry: AnimeEntry = {
id: '123e4567-e89b-12d3-a456-426614174000',
user_id: '550e8400-e29b-41d4-a716-446655440000',
anime_id: 1535,
title: 'Death Note',
title_english: 'Death Note',
title_japanese: 'デスノート',
image: 'https://cdn.myanimelist.net/images/anime/9/9453.jpg',
type: 'TV',
episodes: 37,
status: 'completed',
episodes_watched: 37,
score: 9,
start_date: '2024-01-01',
finish_date: '2024-01-20',
notes: 'Amazing psychological thriller',
tags: ['psychological', 'must-watch'],
favorite: true,
rewatch_count: 1,
priority: null,
genres: ['Mystery', 'Thriller', 'Supernatural'],
year: 2006,
rating: 'R',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-20T12:00:00Z'
};
Review
Detailed anime review with ratings and publication status.
interface Review {
id: string;
user_id: string;
anime_id: number;
rating: number;
story_rating: number | null;
animation_rating: number | null;
sound_rating: number | null;
character_rating: number | null;
enjoyment_rating: number | null;
title: string;
body: string;
spoilers: boolean;
watch_status: 'completed' | 'watching' | 'dropped' | 'plan-to-watch';
episodes_watched: number | null;
tags: string[];
pros: string | null;
cons: string | null;
recommendation: 'highly-recommend' | 'recommend' | 'mixed' | 'not-recommend' | 'strongly-not-recommend' | null;
status: 'draft' | 'published';
helpful_votes: number;
created_at: string;
updated_at: string;
}
Review UUID (auto-generated)
Anime ID from external API
Story/plot rating from 1-10
Animation quality rating from 1-10
Sound/music rating from 1-10
Character development rating from 1-10
Personal enjoyment rating from 1-10
Full review content (supports markdown)
Whether review contains spoilers (default: false)
watch_status
'completed' | 'watching' | 'dropped' | 'plan-to-watch'
required
Watch status when review was written
Episodes watched at time of review
Review tags array (default: empty)
recommendation
'highly-recommend' | 'recommend' | 'mixed' | 'not-recommend' | 'strongly-not-recommend' | null
Recommendation level
status
'draft' | 'published'
required
Publication status (default: ‘draft’)
Net helpful votes (helpful - not helpful, auto-updated)
ISO 8601 timestamp when review was created
ISO 8601 timestamp when review was last updated
Usage:
const review: Review = {
id: '789e0123-e89b-12d3-a456-426614174000',
user_id: '550e8400-e29b-41d4-a716-446655440000',
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 anime that explores...',
spoilers: false,
watch_status: 'completed',
episodes_watched: 37,
tags: ['psychological', 'thriller', 'must-watch'],
pros: 'Brilliant mind games, excellent character development',
cons: 'Second half slightly weaker than first',
recommendation: 'highly-recommend',
status: 'published',
helpful_votes: 42,
created_at: '2024-02-01T10:00:00Z',
updated_at: '2024-02-15T14:30:00Z'
};
ReviewVote
Helpful/not helpful vote on a review.
interface ReviewVote {
id: string;
review_id: string;
user_id: string;
helpful: boolean;
created_at: string;
}
Vote UUID (auto-generated)
Review UUID being voted on
True for helpful, false for not helpful
ISO 8601 timestamp when vote was cast
Usage:
const vote: ReviewVote = {
id: 'abc12345-e89b-12d3-a456-426614174000',
review_id: '789e0123-e89b-12d3-a456-426614174000',
user_id: '550e8400-e29b-41d4-a716-446655440000',
helpful: true,
created_at: '2024-02-05T12:00:00Z'
};
Comment on a review with threading support.
interface Comment {
id: string;
review_id: string;
user_id: string;
parent_id: string | null;
content: string;
created_at: string;
updated_at: string;
deleted_at: string | null;
}
Comment UUID (auto-generated)
Review UUID being commented on
Parent comment UUID for replies (null for top-level)
ISO 8601 timestamp when comment was created
ISO 8601 timestamp when comment was last updated
ISO 8601 timestamp when soft deleted (null if active)
Usage:
// Top-level comment
const comment: Comment = {
id: 'def67890-e89b-12d3-a456-426614174000',
review_id: '789e0123-e89b-12d3-a456-426614174000',
user_id: '550e8400-e29b-41d4-a716-446655440000',
parent_id: null,
content: 'Great review! I totally agree.',
created_at: '2024-02-10T15:30:00Z',
updated_at: '2024-02-10T15:30:00Z',
deleted_at: null
};
// Reply to comment
const reply: Comment = {
id: 'ghi24680-e89b-12d3-a456-426614174000',
review_id: '789e0123-e89b-12d3-a456-426614174000',
user_id: '661f9511-f3e-52e5-b827-557766551111',
parent_id: 'def67890-e89b-12d3-a456-426614174000',
content: 'Thanks for reading!',
created_at: '2024-02-10T16:00:00Z',
updated_at: '2024-02-10T16:00:00Z',
deleted_at: null
};
Upvote/downvote on a comment.
interface CommentVote {
id: string;
comment_id: string;
user_id: string;
upvote: boolean;
created_at: string;
}
Vote UUID (auto-generated)
Comment UUID being voted on
True for upvote, false for downvote
ISO 8601 timestamp when vote was cast
Usage:
const commentVote: CommentVote = {
id: 'jkl13579-e89b-12d3-a456-426614174000',
comment_id: 'def67890-e89b-12d3-a456-426614174000',
user_id: '550e8400-e29b-41d4-a716-446655440000',
upvote: true,
created_at: '2024-02-11T09:00:00Z'
};
CustomList
User-created custom anime list.
interface CustomList {
id: string;
user_id: string;
name: string;
description: string | null;
is_public: boolean;
created_at: string;
updated_at: string;
}
List UUID (auto-generated)
Whether list is publicly visible (default: false)
ISO 8601 timestamp when list was created
ISO 8601 timestamp when list was last updated
Usage:
const list: CustomList = {
id: 'mno97531-e89b-12d3-a456-426614174000',
user_id: '550e8400-e29b-41d4-a716-446655440000',
name: 'Best Psychological Thrillers',
description: 'Mind-bending anime that keep you on edge',
is_public: true,
created_at: '2024-01-05T10:00:00Z',
updated_at: '2024-02-01T14:00:00Z'
};
CustomListEntry
Anime entry within a custom list.
interface CustomListEntry {
id: string;
list_id: string;
anime_id: number;
created_at: string;
}
Entry UUID (auto-generated)
Anime ID from external API
ISO 8601 timestamp when anime was added to list
Usage:
const listEntry: CustomListEntry = {
id: 'pqr86420-e89b-12d3-a456-426614174000',
list_id: 'mno97531-e89b-12d3-a456-426614174000',
anime_id: 1535, // Death Note
created_at: '2024-01-05T11:00:00Z'
};
Extended Types
Types with joined data from related tables.
ReviewWithProfile
Review with author profile information.
interface ReviewWithProfile extends Review {
profiles: Profile;
}
Usage:
const reviewWithProfile: ReviewWithProfile = {
// All Review fields
id: '789e0123-e89b-12d3-a456-426614174000',
user_id: '550e8400-e29b-41d4-a716-446655440000',
anime_id: 1535,
rating: 9,
// ... other review fields
// Joined profile data
profiles: {
id: '550e8400-e29b-41d4-a716-446655440000',
username: 'otaku_master',
avatar_url: 'https://example.com/avatar.jpg',
bio: 'Anime enthusiast',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-03-01T00:00:00Z'
}
};
Comment with author profile and optional nested replies.
interface CommentWithProfile extends Comment {
profiles: Profile;
replies?: CommentWithProfile[];
}
Usage:
const commentWithProfile: CommentWithProfile = {
// All Comment fields
id: 'def67890-e89b-12d3-a456-426614174000',
review_id: '789e0123-e89b-12d3-a456-426614174000',
user_id: '550e8400-e29b-41d4-a716-446655440000',
parent_id: null,
content: 'Great review!',
created_at: '2024-02-10T15:30:00Z',
updated_at: '2024-02-10T15:30:00Z',
deleted_at: null,
// Joined profile data
profiles: {
id: '550e8400-e29b-41d4-a716-446655440000',
username: 'otaku_master',
avatar_url: 'https://example.com/avatar.jpg',
bio: 'Anime enthusiast',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-03-01T00:00:00Z'
},
// Optional nested replies
replies: [
// Array of CommentWithProfile for threaded discussions
]
};
AnimeEntryWithAnime
Anime entry that can be extended with external API data.
interface AnimeEntryWithAnime extends AnimeEntry {
// Can be extended with anime data from external API
}
This type is designed to be extended in your application with data from external anime APIs like MyAnimeList or Jikan.
Type Guards
Helper functions for type checking:
// Check if review is published
function isPublished(review: Review): boolean {
return review.status === 'published';
}
// Check if comment is deleted
function isDeleted(comment: Comment): boolean {
return comment.deleted_at !== null;
}
// Check if entry is completed
function isCompleted(entry: AnimeEntry): boolean {
return entry.status === 'completed';
}
Utility Types
Common TypeScript utility types for database operations:
// Type for creating a new anime entry (without auto-generated fields)
type NewAnimeEntry = Omit<AnimeEntry, 'id' | 'created_at' | 'updated_at'>;
// Type for updating an anime entry (all fields optional except id)
type UpdateAnimeEntry = Partial<AnimeEntry> & { id: string };
// Type for creating a new review
type NewReview = Omit<Review, 'id' | 'created_at' | 'updated_at' | 'helpful_votes'>;
// Type for review with optional id (for upsert operations)
type UpsertReview = Omit<Review, 'created_at' | 'updated_at' | 'helpful_votes'> & { id?: string };
Usage Examples
With Query Functions
import { getAnimeEntries, upsertAnimeEntry } from '@/lib/supabase/queries';
import type { AnimeEntry } from '@/types/database';
// Type-safe query
const entries: AnimeEntry[] = await getAnimeEntries(userId);
// Type-safe upsert
const newEntry: Omit<AnimeEntry, 'id' | 'created_at' | 'updated_at'> = {
user_id: userId,
anime_id: 1535,
title: 'Death Note',
status: 'watching',
episodes_watched: 0,
tags: [],
favorite: false,
rewatch_count: 0,
genres: []
};
const created: AnimeEntry = await upsertAnimeEntry(newEntry);
With React State
import { useState, useEffect } from 'react';
import type { Review, ReviewWithProfile } from '@/types/database';
function ReviewList() {
const [reviews, setReviews] = useState<ReviewWithProfile[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
loadReviews();
}, []);
async function loadReviews() {
// Type-safe API call
const data = await fetchReviewsWithProfiles();
setReviews(data);
setLoading(false);
}
return (
<div>
{reviews.map(review => (
<div key={review.id}>
<h3>{review.title}</h3>
<p>By {review.profiles.username}</p>
<p>Rating: {review.rating}/10</p>
</div>
))}
</div>
);
}
import type { Review } from '@/types/database';
function validateReview(review: Partial<Review>): string[] {
const errors: string[] = [];
if (!review.title || review.title.trim().length === 0) {
errors.push('Title is required');
}
if (!review.body || review.body.trim().length < 100) {
errors.push('Review must be at least 100 characters');
}
if (!review.rating || review.rating < 1 || review.rating > 10) {
errors.push('Rating must be between 1 and 10');
}
const validStatuses: Review['status'][] = ['draft', 'published'];
if (review.status && !validStatuses.includes(review.status)) {
errors.push('Invalid status');
}
return errors;
}
See Also