Where is join request typing handled?

master
0a8cdd4chore(release): version 2.90.1 changelogs (#2012)

Where is the typing of join request handled?example when using .select("projects(user_id)")typing is assuming projects being an [] of object.

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

I'll help you find where the typing of join requests is handled in the supabase-js repository. Let me start by exploring the structure and searching for relevant code.

Let me explore the postgrest-js package more specifically, as it seems to handle the select query parsing:

Now let me look at the select-query-parser result.ts file, which seems to handle the typing of select results:

Let me continue reading the result.ts file to see how join relationships are typed:

Let me continue reading to find the ProcessEmbeddedResource type:

Perfect! Now let me look at the ResolveRelationship utility function to understand how the relationship is determined:

Now let me find the ResolveRelationship function which is the core logic:

Let me try again:

Great! Now let me look at a test file to see concrete examples:

Perfect! Now let me look at one of the test files that specifically tests typing:

Now let me check where the determination of whether something is an array happens. Let me look at the key part in result.ts where it determines if a relation is one-to-one or one-to-many:

Perfect! Now I have a good understanding. Let me check the types.ts file to see how GenericRelationship is defined:

Perfect! Now let me create a comprehensive summary. Let me also check one more test file to show a concrete example:

Let me check the relationships test file for more concrete examples:

Excellent! Now I have all the information I need. Let me provide a comprehensive answer to the user.

##Summary

The typing of join requests (embedded resources) in supabase-js is handled through a sophisticated type system in the postgrest-js package. Here's where and how it works:

###Key Files Involved:

  1. packages/core/postgrest-js/src/select-query-parser/result.ts - Main type inference logic
  2. packages/core/postgrest-js/src/select-query-parser/utils.ts - Relationship resolution
  3. packages/core/postgrest-js/src/types/common/common.ts - Type definitions for relationships

###How It Works:

####1. Relationship Metadata (GenericRelationship)

When you define your schema, each relationship includes an isOneToOne flag:

export type GenericRelationship = {
  foreignKeyName: string
  columns: string[]
  isOneToOne?: boolean  // ← This determines array vs single object
  referencedRelation: string
  referencedColumns: string[]
}

####2. The Core Type: ProcessEmbeddedResourceResult

The main logic that determines whether a field is typed as an array [] is in result.ts at around line 417-422:

type ProcessEmbeddedResourceResult<...> = 
  ProcessNodes<...> extends infer ProcessedChildren
    ? {
        [K in GetFieldNodeResultName<Field>]: Resolved['direction'] extends 'forward'
          ? Field extends { innerJoin: true }
            ? Resolved['relation']['isOneToOne'] extends true
              ? ProcessedChildren           // ← Single object (1-to-1)
              : ProcessedChildren[]         // ← Array (1-to-many)
            : Resolved['relation']['isOneToOne'] extends true
              ? ... // Single object
              : ProcessedChildren[]         // ← Array (1-to-many)

Key Decision Point:

  • If isOneToOne === true: result is ProcessedChildren (single object)
  • If isOneToOne === false or undefined: result is ProcessedChildren[] (array)

####3. Relationship Resolution: ResolveRelationship

The type at line 226 in utils.ts determines what relationship metadata is found for a given field:

export type ResolveRelationship<
  Schema extends GenericSchema,
  Relationships extends GenericRelationship[],
  Field extends Ast.FieldNode,
  CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
>

This function:

  1. Looks up relationships in your schema
  2. Matches the field name to a relationship
  3. Returns the complete relationship object including isOneToOne

###Practical Example:

When you write:

.select("projects(user_id)")

The type system:

  1. Parses projects as a field with children [user_id]
  2. Calls ResolveRelationship to find the projects relationship definition
  3. Checks projects_relationship.isOneToOne:
    • If true: types as { projects: Project }
    • If false: types as { projects: Project[] } ← Your case

###What Determines isOneToOne?

The isOneToOne flag in your database schema comes from your Postgres schema generation (typically via postgres-meta):

  • One-to-many relationships (foreign key on the "many" side): isOneToOne: false → array
  • One-to-one relationships (unique constraint or primary key): isOneToOne: true → single object
  • Many-to-one relationships (reverse joins): handled differently, often returns single object

###Test Example:

From packages/core/postgrest-js/test/relationships.test.ts:

// One-to-many relationship (messages is one-to-many)
.select('username, messages(id)')
// Types as: { username: string, messages: Message[] }

// One-to-one relationship (user is many-to-one on message)
.select('user:users(*)')
// Types as: { user: User }

The magic happens because the generated schema knows that a messages relationship from users is one-to-many, while a reverse users relationship from messages is one-to-one.


END OF POST

Log in to add a comment
Claude Haiku 4.5
Where is join request typing handled? — supabase/supabase-js