Skip to main content

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

  1. Type Safety: Components use types generated from GraphQL schema
  2. Loose Coupling: No direct dependency on Ebisu database schemas
  3. Single Source of Truth: GraphQL schema defines available data
  4. Optimized Queries: Frontend only requests needed fields
  5. Caching: TanStack Query handles caching automatically
  6. 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:

  1. Identify components using Ebisu data
  2. Create appropriate GraphQL queries
  3. Generate TypeScript types
  4. Refactor components to use GraphQL hooks
  5. 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

  1. Searchable Selects: Species, countries, gear types
  2. Hierarchical Data: Taxonomic classifications
  3. Multi-source Data: Gear types from different standards
  4. Filtered Lists: Commercial species only
  5. 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