Source:
ocean/docs/adr/0050-building-components-with-ebisu-data.md| ✏️ Edit on GitHub
ADR-0050: Building Components with Ebisu Data via GraphQL
Status
Accepted
Context
Ocean needs to display and interact with reference data from the Ebisu database (species, countries, gear types, vessels, etc.). With Ebisu maintained as a separate repository (ADR-049), we need clear patterns for building UI components that consume this data without creating tight coupling between the repositories.
The current architecture uses GraphQL as the data interface layer, with Edge Functions handling the connection between Ocean's frontend and Ebisu's PostgreSQL database.
Decision
We will use GraphQL as the exclusive interface for accessing Ebisu data in Ocean components. Components will use TypeScript types generated from the GraphQL schema, not Drizzle ORM schemas or direct database types.
Architecture Overview
Ocean UI Components
↓ (uses generated types)
GraphQL Queries/Mutations
↓ (HTTP/WebSocket)
Supabase Edge Functions (graphql-v2)
↓ (SQL queries)
Ebisu Database (PostgreSQL)
Implementation Patterns
1. Type Generation Workflow
# When GraphQL schema changes
pnpm run codegen:graphql
# This generates:
# - src/generated/graphql.ts (types)
# - src/generated/graphql-operations.ts (typed hooks)
2. Component Pattern
// ❌ DON'T: Import Ebisu database types directly
import { asfisSpecies } from '@ocean/ebisu-database/schema'
// ✅ DO: Use GraphQL generated types
import { useSpeciesQuery } from '@/hooks/graphql/use-species'
import type { Species } from '@/generated/graphql'
export function SpeciesSelector() {
const { data, loading, error } = useSpeciesQuery({
filters: { isCommercial: true },
orderBy: 'scientificName',
})
if (loading) return <SpeciesSkeletonLoader />
if (error) return <ErrorFallback error={error} />
return (
<Select>
{data?.species.map((species) => (
<SelectItem key={species.id} value={species.id}>
{species.scientificName} ({species.alpha3Code})
</SelectItem>
))}
</Select>
)
}
3. Query Hook Pattern
// src/hooks/graphql/use-species.ts
import { useGraphQLQuery } from '@/hooks/use-graphql-query'
import { SPECIES_QUERY } from '@/graphql/queries/species'
import type { SpeciesQueryVariables, SpeciesQuery } from '@/generated/graphql'
export function useSpeciesQuery(variables?: SpeciesQueryVariables) {
return useGraphQLQuery<SpeciesQuery, SpeciesQueryVariables>({
query: SPECIES_QUERY,
variables,
queryKey: ['species', variables],
staleTime: 1000 * 60 * 60, // 1 hour - reference data changes infrequently
})
}
4. GraphQL Query Definition
# src/graphql/queries/species.graphql
query Species($filters: SpeciesFilter, $orderBy: SpeciesOrderBy, $limit: Int, $offset: Int) {
species(filters: $filters, orderBy: $orderBy, limit: $limit, offset: $offset) {
id
asfisId
scientificName
englishName
frenchName
spanishName
alpha3Code
taxonomicCode
isCommercial
family
order
# Only include fields needed by UI
}
}
5. Complex Component Example
// Vessel registration component using multiple Ebisu datasets
export function VesselRegistrationForm() {
const { data: countries } = useCountriesQuery()
const { data: vesselTypes } = useVesselTypesQuery()
const { data: gearTypes } = useGearTypesQuery({
source: 'FAO' // FAO, MSC, or CBP
})
const form = useTanStackForm({
schema: vesselRegistrationSchema,
defaultValues: {
countryCode: '',
vesselType: '',
primaryGearType: '',
},
})
return (
<Form form={form} onSubmit={handleSubmit}>
<FormField name="countryCode">
<CountrySelect countries={countries} />
</FormField>
<FormField name="vesselType">
<VesselTypeSelect types={vesselTypes} />
</FormField>
<FormField name="primaryGearType">
<GearTypeSelect gearTypes={gearTypes} />
</FormField>
</Form>
)
}
6. Caching Strategy
Reference data from Ebisu changes infrequently, so aggressive caching is appropriate:
// Shared query options for reference data
export const REFERENCE_DATA_OPTIONS = {
staleTime: 1000 * 60 * 60 * 24, // 24 hours
gcTime: 1000 * 60 * 60 * 24 * 7, // 7 days (formerly cacheTime)
refetchOnWindowFocus: false,
refetchOnReconnect: false,
}
// Usage
const { data } = useCountriesQuery({
queryOptions: REFERENCE_DATA_OPTIONS,
})
7. Type-Safe Filtering
// GraphQL schema defines available filters
interface SpeciesFilter {
isCommercial?: boolean
family?: string
taxonomicCode?: string
searchTerm?: string // Searches multiple fields
}
// Component uses type-safe filters
function CommercialSpeciesOnly() {
const { data } = useSpeciesQuery({
filters: {
isCommercial: true,
family: 'Scombridae', // Tuna family
},
})
// ...
}
Benefits
- Type Safety: Components use types generated from GraphQL schema
- Loose Coupling: No direct dependency on Ebisu database schemas
- Single Source of Truth: GraphQL schema defines available data
- Optimized Queries: Frontend only requests needed fields
- Caching: TanStack Query handles caching automatically
- Error Boundaries: Consistent error handling with GraphQL errors
Consequences
Positive
- Clear separation between data layer and UI layer
- Type safety throughout the stack
- Easy to mock for testing (mock GraphQL responses)
- Can evolve Ebisu schema without breaking Ocean UI
- Automatic TypeScript types from GraphQL schema
Negative
- Extra abstraction layer (GraphQL)
- Need to maintain GraphQL schema alongside database schema
- Can't use Drizzle's type inference directly
Neutral
- Developers must understand GraphQL patterns
- Need to run codegen when GraphQL schema changes
Migration Path
For existing components that might directly import Ebisu types:
- Identify components using Ebisu data
- Create appropriate GraphQL queries
- Generate TypeScript types
- Refactor components to use GraphQL hooks
- Remove any direct Ebisu imports
Examples
Available Ebisu Data Through GraphQL
Based on the current schema, these datasets are available:
- Geographic/Regulatory: Countries (ISO), FAO areas, RFMOs
- Species: ASFIS commercial species, WoRMS taxonomy, ITIS species
- Vessels: Vessel types, hull materials
- Gear: FAO gear types, MSC gear types, CBP gear categories
- Industry: MSC certified fisheries, country profiles
Common Patterns
- Searchable Selects: Species, countries, gear types
- Hierarchical Data: Taxonomic classifications
- Multi-source Data: Gear types from different standards
- Filtered Lists: Commercial species only
- Reference Lookups: ID to name mappings
References
- ADR-049: Ebisu repository separation
- ADR-012: GraphQL billing architecture (similar patterns)
/docs/patterns/data-fetching.md: Data fetching patterns/supabase/functions/graphql-v2/schema.graphql: Current GraphQL schema