DEV Community

Olivier
Olivier

Posted on

Supabase helper for better RPC function typing with jsonb fields

The problem

When you have those tables used for say vector search... you usually have a jsonb field for metadata like this:

create table
  documents (
    id text not null,
    vector extensions.vector null,
    content text not null,
     ---v--- this field
    metadata jsonb null, 
    constraint documents_pkey primary key (id)
  )
Enter fullscreen mode Exit fullscreen mode

Where let's say you always have this in your jsonb metadata

 type Metadata = { 
    sourceUrl: string
    otherField: string
 }
Enter fullscreen mode Exit fullscreen mode

But when you do a supabase call say supabase.rpc('match_vec_doc', args)... even if you create client supabse with Database from the typings provided... that metadata field will be Json and not of type Metadata.. which messes with your rest of your code!

So here is some help functions in addition to the type pulling you can get from

supabase gen types typescript --project-id [PROJECT_ID] > database.types.ts
Enter fullscreen mode Exit fullscreen mode

The solution

Create a database_types.ts file in say you lib/supabase folder, and import from the generated types (you probably have some of this for Tables, Views):

import { type Database } from '@/database.types'

export type Tables<T extends keyof Database['public']['Tables']> =
  Database['public']['Tables'][T]['Row']
export type Views<T extends keyof Database['public']['Views']> =
  Database['public']['Views'][T]['Row']
export type Enums<T extends keyof Database['public']['Enums']> =
  Database['public']['Enums'][T]

export type Insert<T extends keyof Database['public']['Tables']> =
  Database['public']['Tables'][T]['Insert']

export type Functions = Database['public']['Functions']
Enter fullscreen mode Exit fullscreen mode

Then add a MetadataField type mapping

// (... continue)
// Keys are the types for each metadata field here for document
export type MetadataField = {
  documentMetadata: { 
    sourceUrl: string
    otherField: string
  }
  defaultMetadata: unknown // this is the default for all other functions
 }

// Define a type that maps function names to keys in MetadataField
// Here I have 2 functions that both return metadata from the documents tables.. I have the k in keyof Functions to ensure we load all functions to avoid type errors
export type MetadataFieldMapping = {
  [k in keyof Functions]: string
} & {
  match_fts_doc: 'documentMetadata'
  match_vec_doc: 'documentMetadata'
  hybrid_search_doc: 'documentMetadata'
  match_fts_chunk: 'chunkMetadata'
  match_vec_chunk_text: 'chunkMetadata'
}
Enter fullscreen mode Exit fullscreen mode

Then add an RPC type that you will use in your helper replacement function for rpc calls. It has a fallback

//  Define the RPC type using a generic parameter for dynamic metadata field
export type RPC = {
  [FunctionName in keyof MetadataFieldMapping]: {
    Args: Functions[FunctionName]['Args']
    Returns: Array<
      Functions[FunctionName]['Returns'][number] & {
        metadata: MetadataField[MetadataFieldMapping[FunctionName] extends keyof MetadataField
          ? MetadataFieldMapping[FunctionName]
          : 'defaultMetadata'] // Fallback to 'defaultMetadata' if the mapping does not exist
      }
    >
  }
}
Enter fullscreen mode Exit fullscreen mode

The helper function

// Helper function to simplify RPC calls
export async function supabaseRPC<
  MethodName extends keyof MetadataFieldMapping
>(methodName: MethodName, args: RPC[MethodName]['Args']) {
  const supabase = createClient()
  return supabase.rpc<MethodName, RPC[MethodName]>(methodName, args)
}
Enter fullscreen mode Exit fullscreen mode

How to use it

Instead of

supabase.rpc('match_vec_doc', args)
Enter fullscreen mode Exit fullscreen mode

Use

supabaseRPC('match_vec_doc', args)
Enter fullscreen mode Exit fullscreen mode

Example

You can see the metadata field is now properly typed. Note this example the schema is different than above.. but you get it.

Screenshot of using it in some code

Top comments (0)