Source:
ocean/docs/adr/0051-ebisu-graphql-integration.md| ✏️ Edit on GitHub
ADR-0051: GraphQL Integration for Ebisu Reference Data
Status
Accepted
Context
Ocean needs to display and interact with extensive reference data from the Ebisu database, including:
- Geographic data (countries, FAO areas, RFMOs)
- Vessel types and hull materials
- Gear types (FAO, CBP, MSC standards)
- Species taxonomies (ASFIS, WoRMS, ITIS)
- Industry data (MSC fisheries, country profiles)
With Ebisu maintained as a separate repository (ADR-049), we faced a critical decision: how should Ocean components access this data without creating tight coupling between the repositories?
The challenge was that without exposing Ebisu data through our API layer, developers would need to:
- Import Drizzle schemas directly from Ebisu
- Handle cross-repository TypeScript compilation
- Manage database connections from the frontend
- Deal with version mismatches between repositories
Decision
We will expose ALL Ebisu reference data through Ocean's GraphQL API. This creates a clean, typed interface between Ocean's UI components and Ebisu's data warehouse.
Implementation Approach
-
Comprehensive Schema Extension
- Created
schema-ebisu.tswith complete type definitions for all Ebisu tables - Exposed all reference data types through GraphQL
- Maintained consistent naming with Ebisu's database schema
- Created
-
Dedicated Resolvers
- Created
ebisu-resolvers.tswith query resolvers for all data types - Implemented filtering, searching, and pagination
- Added relationship resolvers for linked data (e.g., country profiles → countries)
- Created
-
Connection Architecture
- Ebisu database connection configured via
EBISU_DATABASE_URLenvironment variable - Connection pooling managed at the Edge Function level
- Separate database client in GraphQL context
- Ebisu database connection configured via
Extension Process for New Ebisu Types
When new reference data is added to Ebisu, follow these steps:
1. Update GraphQL Schema
Add the new type to schema-ebisu.ts:
type NewReferenceType {
id: ID!
source_id: ID
field_one: String!
field_two: String
created_at: String!
updated_at: String!
source: OriginalSource
}
extend type Query {
new_reference_types(
search: String
filter_field: String
limit: Int
offset: Int
): [NewReferenceType!]!
new_reference_type(id: ID!): NewReferenceType
}
2. Add Resolver
Update ebisu-resolvers.ts:
new_reference_types: async (_: unknown, args: any, context: Context) => {
const client = await getEbisuClient(context)
let query = 'SELECT * FROM new_reference_table WHERE 1=1'
const params: any[] = []
// Add filters based on args
if (args.search) {
params.push(`%${args.search}%`)
query += ` AND name ILIKE $${params.length}`
}
// Add pagination
if (args.limit) {
params.push(args.limit)
query += ` LIMIT $${params.length}`
}
const result = await client.queryObject(query, params)
return result.rows
}
3. Generate TypeScript Types
Run the GraphQL code generation:
pnpm run codegen:graphql
This generates typed interfaces in src/generated/graphql.ts.
4. Create Query Hook
Add a custom hook for the new data type:
// src/hooks/graphql/use-new-reference-type.ts
import { useGraphQLQuery } from '@/hooks/use-graphql-query'
const NEW_REFERENCE_TYPE_QUERY = `
query GetNewReferenceTypes($search: String, $limit: Int) {
new_reference_types(search: $search, limit: $limit) {
id
field_one
field_two
}
}
`
export function useNewReferenceTypeQuery(variables?: any) {
return useGraphQLQuery({
query: NEW_REFERENCE_TYPE_QUERY,
variables,
queryKey: ['new-reference-types', variables],
staleTime: 1000 * 60 * 60 * 24, // 24 hours for reference data
})
}
5. Build Components
Use the generated types and hooks in components:
import { useNewReferenceTypeQuery } from '@/hooks/graphql/use-new-reference-type'
import type { NewReferenceType } from '@/generated/graphql'
export function NewReferenceTypeSelector() {
const { data, loading, error } = useNewReferenceTypeQuery()
// Component implementation using typed data
}
Consequences
Positive
- Type Safety: Full TypeScript types generated from GraphQL schema
- Clean Architecture: Clear separation between data and UI layers
- Single Source of Truth: GraphQL schema defines the API contract
- Performance: Built-in caching with TanStack Query
- Flexibility: Easy to add new fields or types without breaking existing code
- Developer Experience: IntelliSense and type checking for all Ebisu data
Negative
- Schema Maintenance: Must update GraphQL schema when Ebisu schema changes
- Additional Layer: GraphQL adds complexity compared to direct database access
- Resolver Code: Need to write resolvers for each data type
- Testing: Requires mocking GraphQL responses for component tests
Neutral
- Code Generation: Must run codegen when schema changes
- Learning Curve: Developers must understand GraphQL patterns
- Performance Overhead: GraphQL parsing adds minimal latency
Implementation Details
Current Exposed Data Types
Geographic & Regulatory:
- Countries (ISO codes, names)
- FAO Major Areas
- RFMOs (Regional Fisheries Management Organizations)
Vessel Data:
- Vessel Types (ISSCFV classification)
- Hull Materials
Gear Types:
- FAO Gear Types (ISSCFG)
- CBP Gear Types
- MSC Gear Types
Species Taxonomies:
- ASFIS Species (commercial species)
- WoRMS Taxon (marine species taxonomy)
- ITIS Species (integrated taxonomic information)
Industry Data:
- MSC Certified Fisheries
- Country Profiles (fisheries statistics)
Query Patterns
All queries support:
- Pagination (
limit,offset) - Search functionality
- Field-specific filtering
- Relationship resolution
Caching Strategy
Reference data changes infrequently, so we use aggressive caching:
const REFERENCE_DATA_OPTIONS = {
staleTime: 1000 * 60 * 60 * 24, // 24 hours
gcTime: 1000 * 60 * 60 * 24 * 7, // 7 days
refetchOnWindowFocus: false,
refetchOnReconnect: false,
}
Example: Vessel Type Lookup
Here's a complete example of building a vessel type lookup component:
// GraphQL Query
query VesselTypes($search: String) {
vessel_types(search: $search, limit: 100) {
id
vessel_type_cat
vessel_type_subcat
vessel_type_isscfv_code
vessel_type_isscfv_alpha
}
}
// Component
export function VesselTypeLookup({ onSelect }: { onSelect: (type: VesselType) => void }) {
const [search, setSearch] = useState('')
const { data, loading } = useVesselTypesQuery({ search })
return (
<>
<Input
placeholder="Search vessel types..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{loading ? (
<LoadingSpinner />
) : (
<div>
{data?.vessel_types.map((type) => (
<button
key={type.id}
onClick={() => onSelect(type)}
className="block w-full text-left p-2 hover:bg-accent"
>
<div className="font-medium">{type.vessel_type_cat}</div>
<div className="text-sm text-muted-foreground">
{type.vessel_type_subcat} • {type.vessel_type_isscfv_code}
</div>
</button>
))}
</div>
)}
</>
)
}
Migration Path
For any existing components that might be importing Ebisu types directly:
- Remove direct imports from
@ocean/ebisu-database - Add appropriate GraphQL queries
- Generate TypeScript types
- Update components to use GraphQL hooks
- Test with mocked GraphQL responses
Security Considerations
- Ebisu connection uses read-only credentials
- All queries are parameterized to prevent SQL injection
- GraphQL depth limiting prevents expensive queries
- Authentication still required for GraphQL endpoint
- Row-level security not needed (reference data is public)
References
- ADR-049: Ebisu repository separation
- ADR-050: Building components with Ebisu data
- GraphQL Best Practices: https://graphql.org/learn/best-practices/
- TanStack Query Documentation: https://tanstack.com/query/latest