Skip to content

Better introspection of table relationships#19

Merged
brainkim merged 3 commits intomainfrom
feat/reverse-relations
Jan 5, 2026
Merged

Better introspection of table relationships#19
brainkim merged 3 commits intomainfrom
feat/reverse-relations

Conversation

@brainkim
Copy link
Copy Markdown
Member

@brainkim brainkim commented Jan 4, 2026

Summary

  • Adds relations() method to navigate both forward and reverse table relations
  • Uses WeakMap registry with lazy resolution to handle circular references
  • Forward relations come from as, reverse from reverseAs
  • Validates uniqueness of relation names per target table

API

const Users = table("users", {
  id: z.string().db.primary(),
  email: z.string().email(),
});

const Posts = table("posts", {
  id: z.string().db.primary(),
  title: z.string(),
  authorId: z.string().db.references(Users, "author", { reverseAs: "posts" }),
});

// Forward relation (many-to-one): Posts -> Users
Posts.relations().author.table           // Users table
Posts.relations().author.fields().email  // Users.email field metadata

// Reverse relation (one-to-many): Users -> Posts
Users.relations().posts.table            // Posts table
Users.relations().posts.fields().title   // Posts.title field metadata

// Chain navigation
Users.relations().posts.fields().author.fields().email  // Back to Users

Relation interface

interface Relation<TargetTable> {
  fields(): TableFields<...>;  // Navigate to related table's fields
  table: TargetTable;          // Direct access to related table
}

Test plan

  • Returns empty object for table with no relations
  • Returns forward relation from references
  • Returns reverse relation when defined with reverseAs
  • Returns both forward and reverse relations
  • fields() navigates to related table fields
  • Supports multiple reverse relations
  • Chained navigation through relations
  • No reverse relation without reverseAs
  • Throws on duplicate reverseAs names for same target
  • Throws when reverseAs collides with forward relation name

Closes #18

🤖 Generated with Claude Code

Add `schema` and `db` properties to FieldMeta for direct access to raw
Zod schema and database metadata. Mark existing cooked properties as
deprecated.

New API (preferred):
```typescript
fields.email.schema     // ZodType - use Zod APIs for introspection
fields.email.db         // FieldDBMeta - raw db metadata
fields.email.db.unique  // boolean
```

Old API (deprecated, still works):
```typescript
fields.email.unique     // boolean
fields.email.type       // "email"
fields.email.required   // boolean
```

This is additive and non-breaking. Deprecated properties will be removed
in a future major version when form logic moves to @b9g/forms.

Related to #15

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@brainkim brainkim force-pushed the feat/reverse-relations branch 4 times, most recently from d27c902 to 0552e9e Compare January 5, 2026 02:44
Adds the ability to navigate both forward and reverse relations via a
unified `relations()` method.

- Forward relations come from `as` in `.db.references(Table, "as")`
- Reverse relations come from `reverseAs` in `.db.references(Table, "as", { reverseAs: "..." })`

Implementation:
- Add reverse relations registry using WeakMap for lazy resolution
- Register reverse relations when a reference defines `reverseAs`
- Merge `Relation` and `ReverseRelation` into single `Relation` interface
- Add `relations()` method that returns both forward and reverse
- Validate uniqueness of relation names (as + reverseAs) per target table

Usage:
```typescript
const Posts = table("posts", {
  authorId: z.string().db.references(Users, "author", { reverseAs: "posts" }),
});

// Forward relation (many-to-one): Posts -> Users
Posts.relations().author.table          // Users table
Posts.relations().author.fields().email // Users fields

// Reverse relation (one-to-many): Users -> Posts
Users.relations().posts.table           // Posts table
Users.relations().posts.fields().title  // Posts fields
```

Closes #18

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@brainkim brainkim force-pushed the feat/reverse-relations branch from 0552e9e to 369d14b Compare January 5, 2026 03:01
@brainkim brainkim changed the title feat: add reverses() method for one-to-many relation navigation Better introspection of table relationships Jan 5, 2026
- Update Field Metadata section with new preferred API (schema, db)
- Remove deprecated FieldType from Types section
- Add Relation type to exports documentation
- Add chained relation navigation example
- Fix "Why Zen?" heading to "Why ZenDB?"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@brainkim brainkim merged commit 1d93a47 into main Jan 5, 2026
1 check passed
@brainkim brainkim deleted the feat/reverse-relations branch January 5, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC: Reverse relation navigation in fields()

1 participant