Skip to content

Add RelationRegistry API for custom relations - #1186

Open
Melchyore wants to merge 6 commits into
adonisjs:22.xfrom
Melchyore:feat/custom-relations-api
Open

Add RelationRegistry API for custom relations#1186
Melchyore wants to merge 6 commits into
adonisjs:22.xfrom
Melchyore:feat/custom-relations-api

Conversation

@Melchyore

@Melchyore Melchyore commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

❓ Type of change

  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

Add RelationRegistry API for custom relations

Summary

This PR introduces a new RelationRegistry API that allows third-party packages to register custom relation types in Lucid ORM. This unlocks powerful extensibility while maintaining full type safety and seamless integration with existing Lucid features (preload, whereHas, withCount, serialization, etc.).

Motivation

Currently, Lucid supports five built-in relation types (hasOne, hasMany, belongsTo, manyToMany, hasManyThrough). However, users frequently request specialized relations like:

  • Polymorphic relations (morphTo, morphMany, morphToMany)
  • Hierarchical relations (ancestors, descendants, tree structures)
  • Custom join strategies (lateral joins, recursive CTEs)

Without an extension API, these features would require:

  1. Forking Lucid (not sustainable)
  2. Monkey-patching internal APIs (fragile)
  3. Building separate ORMs (ecosystem fragmentation)

This PR solves the problem by providing an official, supported API for custom relations.


Changes Overview

1. Core API: RelationRegistry Class

New file: src/orm/relations/relation_registry.ts

A centralized registry for managing custom relation types.

Key methods:

  • register(type, factory) - Register a new relation type
  • get(type) - Retrieve a relation factory
  • has(type) - Check if a relation type exists
  • getManyRelationTypes() - Get all "many" relation types (for preloader)
  • unregister(type) - Remove a relation (for testing)
  • clear() - Clear all custom relations (for testing)

Design decisions:

  1. Static methods - Relations are global, no need for instances
  2. Factory pattern - Registry stores factories, not instances

Why this approach?

  • Simple API surface (6 methods)
  • No breaking changes to existing code
  • Easy to test (can clear registry between tests)
  • Type-safe (TypeScript knows all registered relations)

2. Helper: createRelationDecorator Function

New file: src/orm/decorators/create_relation_decorator.ts

A utility to create type-safe relation decorators.

Usage:

// In your custom relation package
export const myRelation = createRelationDecorator('myRelation')

// Users can now use it like built-in relations
class User extends BaseModel {
  @myRelation(() => Post, { /* options */ })
  declare posts: MyRelation<typeof Post>
}

Type constraints:

  • Generic parameter TRelationType is constrained to ModelRelationTypes['__opaque_type']
  • This ensures only registered relation types can be used
  • Provides autocomplete for relation type names

Why a helper?

  • Eliminates boilerplate (decorators are verbose)
  • Ensures consistency with built-in decorators
  • Maintains type safety across custom relations

3. Type System Integration

Modified file: src/types/relations.ts

Added two new interfaces for module augmentation:

A. KnownCustomRelations

export interface KnownCustomRelations {}

Purpose: Register relation contracts (implementation classes)

Used in: RelationshipsContract union type

Contains: Relation class types with methods like boot(), eagerQuery(), setRelated()

Example augmentation:

declare module '@adonisjs/lucid/types/relations' {
  interface KnownCustomRelations {
    myRelation: MyRelationContract<LucidModel, LucidModel>
  }
}

B. KnownCustomOpaqueRelations

export interface KnownCustomOpaqueRelations {}

Purpose: Register relation opaque types (user-facing types)

Used in: ModelRelations union type

Contains: Opaque array types representing the relation data

Example augmentation:

declare module '@adonisjs/lucid/types/relations' {
  interface KnownCustomOpaqueRelations {
    myRelation: MyRelation<LucidModel>  // e.g., Post[] with extra properties
  }
}

Why Two Interfaces?

They serve different roles in the type system:

Interface Purpose Used By Contains
KnownCustomRelations Internal Lucid operations Preloader, serializer, $getRelation() Class contracts with methods
KnownCustomOpaqueRelations User model declarations Model properties, query results Opaque data types (arrays)

4. Factory Types

Modified file: src/types/relations.ts

Added two new interfaces for relation factories:

A. RelationFactoryConfig

export interface RelationFactoryConfig<T> {
  isMany: boolean
  create(relationName, relatedModel, options, model): T
}

Purpose: Configuration passed to RelationRegistry.register()

B. RelationFactory

export interface RelationFactory<T> extends RelationFactoryConfig<T> {
  type: string  // Injected by registry
}

Purpose: Complete factory stored in registry

Note: Includes type property

Why separate?

  • Eliminates redundant type specification
  • Registry injects type automatically
  • Cleaner API for users

5. Extensible Union Types

Modified file: src/types/relations.ts

Made union types extensible by including custom relations:

Before (closed unions):

type ModelRelations = HasOne | HasMany | BelongsTo | ManyToMany | HasManyThrough
type RelationshipsContract = HasOneContract | HasManyContract | ...

❌ Cannot add custom relations

After (open unions):

type ModelRelations =
  | HasOne
  | HasMany
  | BelongsTo
  | ManyToMany
  | HasManyThrough
  | KnownCustomOpaqueRelations[keyof KnownCustomOpaqueRelations]  // ✅ Extensible

type RelationshipsContract =
  | HasOneRelationContract
  | HasManyRelationContract
  | ...
  | KnownCustomRelations[keyof KnownCustomRelations]  // ✅ Extensible

Why this matters:

  • TypeScript can now type-check custom relations
  • Autocomplete works for custom relation methods
  • No any types needed

6. BaseModel Integration

Modified file: src/orm/base_model/index.ts

Updated $addRelation() to support custom relations:

Changes:

  1. Refactored built-in relation handling

    private static $addBuiltInRelation(name, type, relatedModel, options) {
      // Existing switch statement for hasOne, hasMany, etc.
      // Returns relation if matched, null otherwise
    }
  2. Check registry for custom relations

    static $addRelation(name, type, relatedModel, options) {
      // Try built-in first
      const builtIn = this.$addBuiltInRelation(name, type, relatedModel, options)
      if (builtIn) return builtIn
    
      // Check registry for custom relations
      const factory = RelationRegistry.get(type)
      if (!factory) {
        throw new Error(`"${type}" is not a supported relation type. Did you forget to register it?`)
      }
    
      const relation = factory.create(name, relatedModel, options, this)
      this.$relationsDefinitions.set(name, relation)
      return relation
    }
  3. Replaced static MANY_RELATIONS with getManyRelations() to support custom relations

    // Before (static)
    const MANY_RELATIONS = ['hasMany', 'manyToMany', 'hasManyThrough']  // ❌ Computed once at module load
    
    // After (dynamic)
    function getManyRelations(): string[] {
      return ['hasMany', 'manyToMany', 'hasManyThrough', ...RelationRegistry.getManyRelationTypes()]
    }
    // ✅ Includes newly registered relations

Why these changes?

  • Backward compatible (built-in relations work unchanged)
  • Clear error messages for unregistered relations
  • Dynamic discovery of "many" relations for preloader
  • No performance impact (registry lookup is O(1))

7. Type System Compatibility

Modified files:

  • src/types/model.ts - Updated $relationsDefinitions signature
  • src/types/relations.ts - Added setRelated, pushRelated, setRelatedForMany to BaseRelationContract
  • src/factories/factory_model.ts - Added type assertions for backward compatibility

Why needed?

  • Ensure custom relations work with all Lucid features
  • Maintain strict TypeScript checking
  • Support model factories with custom relations

8. Exports

Modified file: src/orm/main.ts

Added new exports:

export { createRelationDecorator } from './decorators/create_relation_decorator.js'
export { RelationRegistry } from './relations/relation_registry.js'
export type { RelationFactory, RelationFactoryConfig } from '../types/relations.js'

Modified file: src/orm/relations/main.ts

export { BaseQueryBuilder as RelationBaseQueryBuilder } from './base/query_builder.js'
export { BaseSubQueryBuilder as RelationBaseSubQueryBuilder } from './base/sub_query_builder.ts'
export { KeysExtractor } from './keys_extractor.js'

9. Tests

New file: test/orm/relation_registry.spec.ts

Comprehensive test coverage:

  • ✅ Register custom relation
  • ✅ Prevent duplicate registration
  • ✅ Get/has methods work correctly
  • ✅ getManyRelationTypes filters correctly
  • ✅ Unregister removes relations
  • ✅ Integration with BaseModel.$addRelation
  • ✅ createRelationDecorator works
  • ✅ Custom relations work with preload
  • ✅ Custom "many" relations work correctly

Breaking Changes

None. This PR is 100% backward compatible.

  • Existing relations work unchanged
  • No API changes to user-facing code
  • No database schema changes

Performance Considerations

  1. Registry lookup: O(1) Map lookup, negligible overhead
  2. Dynamic MANY_RELATIONS: Called once per model boot, minimal impact
  3. Type checking: Zero runtime cost (TypeScript only)
  4. Memory: Minimal (stores factory functions, not instances)

Type Safety

This PR maintains full type safety:

✅ Autocomplete for custom relation names in $addRelation()

✅ Type-checked relation options

✅ Inferred return types for relation queries

✅ Autocomplete in preload callbacks

✅ Type-safe access to related models

How?

  • Module augmentation extends Lucid's type system
  • Opaque types preserve relation type information
  • Constrained generics prevent invalid relation types

Testing Checklist

  • Unit tests for RelationRegistry
  • Integration tests with BaseModel
  • Type checking tests (TypeScript compilation)
  • Real-world example (ancestors relation)
  • Documentation with complete example
  • Backward compatibility verified
  • Performance benchmarks (no regression)

Summary of Files Changed

New Files (6)

  • src/orm/relations/relation_registry.ts - Core registry implementation
  • src/orm/decorators/create_relation_decorator.ts - Decorator helper
  • test/orm/relation_registry.spec.ts - Registry tests

Modified Files (6)

  • src/orm/base_model/index.ts - Custom relation support in $addRelation
  • src/orm/main.ts - Export new APIs
  • src/types/relations.ts - Type augmentation interfaces, extensible unions
  • src/types/model.ts - Updated signatures
  • src/factories/factory_model.ts - Type compatibility
  • src/orm/relations/main.ts - (if any changes)

Other notes

  • If this PR is accepted, I'll open another PR to backport these features to v21, and another for the documentation.

Summary by CodeRabbit

  • New Features

    • Added support for registering and using custom relationship types.
    • Added reusable tools for creating custom relationship decorators.
    • Exposed relation registry and query utilities through the public API.
    • Added clearer type information for singular and collection relationships.
  • Bug Fixes

    • Improved relationship preloading for custom singular relationships.
    • Added clear errors for unsupported or duplicate relationship types.
  • Tests

    • Added coverage for custom relationships, registry isolation, registration errors, and preloading behavior.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds per-model relation registries, custom relation decorators, extensible relation contracts, and cardinality-aware relation handling. Built-in relations resolve through registry factories, while custom singular relations preload as single values.

Changes

Custom relation support

Layer / File(s) Summary
Relation contracts and multiplicity
src/types/relations.ts, src/orm/relations/*/index.ts
Relation factory and registry contracts support custom types. Relation contracts expose literal type and isMany values. Built-in relation classes declare their cardinality.
Relation registry storage
src/orm/relations/relation_registry.ts, src/errors.ts
Each registry seeds the five built-in factories and supports isolated registration, lookup, and duplicate-type errors.
Model relation resolution and assignment
src/orm/base_model/index.ts, src/types/model.ts, src/orm/decorators/create_relation_decorator.ts, src/orm/preloader/index.ts
Models resolve relations through $relationRegistry and $addRelation. Custom decorators register relation metadata. Unsupported types throw an error. Preload and assignment logic use isMany.
Public exports and integration validation
src/orm/main.ts, src/orm/relations/main.ts, test/orm/relation_registry.spec.ts, test/orm/custom_relations.spec.ts, test/orm/orm_schema_builder.spec.ts
New relation APIs are exported. Tests cover registry behavior, custom singular preloading, type narrowing, unsupported types, and existing schema cases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Decorator as createRelationDecorator
  participant Model as BaseModelImpl
  participant Registry as RelationRegistry
  participant Factory as RelationFactory
  Decorator->>Model: register relation with $addRelation
  Model->>Registry: retrieve relation factory
  Registry-->>Model: return factory
  Model->>Factory: create relation
  Factory-->>Model: return relation instance
  Model->>Model: store relation definition
Loading

Possibly related PRs

Suggested reviewers: thetutlage

Poem

A rabbit hops through relation rows,
A registry guides where each one goes.
Singular hops stay neat and small,
Many hops gather in a hall.
Custom types now join the flow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding a RelationRegistry API for custom relations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/factories/factory_model.ts (1)

171-191: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail explicitly for unsupported custom factory relations.

A registered custom relation reaches this switch but matches no case, so relation() returns with no relation registered. Add a default error (or a custom factory adapter contract) rather than silently dropping the requested relation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/factories/factory_model.ts` around lines 171 - 191, Update the
relation-type switch in the factory relation registration method to add a
default branch that throws an explicit error for unsupported custom relation
types. Ensure unmatched types do not return silently or leave the requested
relation unregistered, while preserving the existing built-in relation handling
and hasManyThrough error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/orm/base_model/index.ts`:
- Around line 76-85: Update relation definition/instance handling and
$setRelated() so each relation retains its many-versus-one multiplicity when
created, rather than deriving it from the mutable RelationRegistry at assignment
time. Keep getManyRelations() for discovering registered custom relation types,
but ensure RelationRegistry.unregister(type) cannot change the behavior of
existing relations or cause valid arrays to be rejected.

In `@src/orm/relations/main.ts`:
- Around line 14-16: Update the BaseSubQueryBuilder re-export in the relations
entry point to use the emitted .js module specifier, matching the existing
BaseQueryBuilder and KeysExtractor exports; leave the exported symbol alias
unchanged.

In `@src/orm/relations/relation_registry.ts`:
- Around line 32-44: Update RelationRegistry.register to reject reserved
built-in relation type names such as hasOne and hasMany before checking for
duplicates or storing the factory. Reuse the existing built-in relation-name
definition if available, and throw a clear error for reserved names while
preserving normal registration for custom types.

---

Outside diff comments:
In `@src/factories/factory_model.ts`:
- Around line 171-191: Update the relation-type switch in the factory relation
registration method to add a default branch that throws an explicit error for
unsupported custom relation types. Ensure unmatched types do not return silently
or leave the requested relation unregistered, while preserving the existing
built-in relation handling and hasManyThrough error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7912a62d-5cce-463a-bd4a-7fd05aacfa6e

📥 Commits

Reviewing files that changed from the base of the PR and between b23d814 and 66bc92e.

📒 Files selected for processing (9)
  • src/factories/factory_model.ts
  • src/orm/base_model/index.ts
  • src/orm/decorators/create_relation_decorator.ts
  • src/orm/main.ts
  • src/orm/relations/main.ts
  • src/orm/relations/relation_registry.ts
  • src/types/model.ts
  • src/types/relations.ts
  • test/orm/relation_registry.spec.ts

Comment thread src/orm/base_model/index.ts Outdated
Comment thread src/orm/relations/main.ts
Comment thread src/orm/relations/relation_registry.ts Outdated
Relations now retain their isMany property on the relation instance itself rather than deriving it from mutable RelationRegistry state at assignment time.

Previously, if a custom relation was unregistered after model boot, $setRelated would incorrectly reject arrays because it looked up isMany from the registry. Now each relation instance stores its multiplicity, making it independent of registry state changes.

Changes:
- Add readonly isMany property to BaseRelationContract
- Add isMany = true/false to all built-in relation classes
- Replace getManyRelations().includes() checks with relation.isMany
- Remove isMany from RelationFactoryConfig (no longer needed)
- Remove RelationRegistry.getManyRelationTypes() (no longer needed)
- Add test verifying relations retain multiplicity after unregister

This ensures RelationRegistry.unregister() cannot change the behavior of existing relation instances or cause valid arrays to be rejected.
RelationRegistry.register() now rejects reserved built-in relation type names (hasOne, hasMany, belongsTo, manyToMany, hasManyThrough).

Attempting to register these names is ineffective because BaseModel.$addRelation() resolves built-ins first, so the registry entry would never be used. This prevents confusion and registry pollution.

Changes:
- Add BUILT_IN_TYPES constant in RelationRegistry.register()
- Throw clear error when attempting to register reserved names
- Add test verifying all built-in types are rejected
@Melchyore

Copy link
Copy Markdown
Contributor Author

I've addressed all issues:

1. Relation multiplicity stored on instances (not registry lookup)

Problem: Relations derived their many-vs-one multiplicity from RelationRegistry.getManyRelationTypes() at assignment time. If a custom relation was unregistered after model boot, $setRelated() would incorrectly reject arrays because the type was no longer in the registry.

Solution: Each relation instance now has a readonly isMany: boolean property. Built-in relations define it directly in their class (HasMany.isMany = true, HasOne.isMany = false, etc.). Custom relations do the same. The $setRelated() and $pushRelated() methods now check relation.isMany directly instead of looking it up.

Benefits:

  • Relations are independent of registry state changes
  • RelationRegistry.unregister() cannot affect existing relations
  • Simpler API: no need for isMany in factory config
  • Better encapsulation: multiplicity is part of the relation contract

Changes:

  • Added isMany to BaseRelationContract
  • Added isMany to all built-in relation classes
  • Removed isMany from RelationFactoryConfig
  • Removed getManyRelationTypes() (no longer needed)
  • Replaced registry lookups with relation.isMany checks
  • Added regression test

2. Reject built-in relation type names

Problem: Registering hasOne, hasMany, etc. is ineffective because BaseModel.$addRelation() resolves built-ins first. The registry entry would never be used, causing confusion.

Solution: RelationRegistry.register() now throws an error for reserved built-in type names before checking duplicates or storing the factory.

Changes:

  • Added BUILT_IN_TYPES constant
  • Clear error message listing all reserved names
  • Added test for all built-in types

Comment thread src/factories/factory_model.ts Outdated
Comment thread src/types/relations.ts Outdated
…egistry

Reworks the RelationRegistry API so that extensibility does not come at the
cost of the type system, and so that custom relations travel the same code
path as the built-in ones.

Type system

  Folding "KnownCustomRelations[keyof KnownCustomRelations]" straight into
  "RelationshipsContract" destroyed discriminated-union narrowing: the
  registered contracts extend "BaseRelationContract", whose "type" is the
  full union, so such a member survives every `case` of a
  `switch (relation.type)`. That is why four "as any" casts had to be added
  to factory_model.ts -- the test file's own augmentation was enough to
  break narrowing inside Lucid.

  BaseRelationContract is now generic over its discriminant and its
  multiplicity, so every relation carries literals:

    BaseRelationContract<ParentModel, RelatedModel, Type, IsMany>

  Built-in contracts parameterise it ('hasOne', false) instead of
  redeclaring "type", "isMany", "setRelated" and "pushRelated" by hand,
  which removes 15 duplicated declarations. Custom relations use the very
  same contract -- there is no separate one to learn.

  Entries are then admitted to the unions only when their "type" (or
  "__opaque_type") matches the key they were registered under. A malformed
  augmentation collapses to "never" instead of silently disabling narrowing
  for every consumer of Lucid.

  "$relationsDefinitions" goes back to Map<string, RelationshipsContract>.
  Widening it to BaseRelationContract lost every per-relation member and
  made "setRelated(parent, [a, b])" type-check against a hasOne. A single
  documented cast at the registry boundary replaces it.

Registry

  Built-in relations are ordinary registry entries, seeded in the
  constructor. All five already share the same constructor signature, so
  "$addBuiltInRelation" and its switch were pure ceremony; "$addRelation"
  is now one lookup. "$addHasOne" and friends are kept for backwards
  compatibility and delegate to it, so there is still only one place a
  relation gets constructed.

  Registration is monotonic: a type can be added, never removed, replaced
  or shadowed -- for built-in and custom relations alike. "unregister" and
  "clear" could not undo anything anyway, because booted models hold
  relation instances rather than factories; they only broke the next module
  to define that relation. Isolation now comes from replacing the registry
  ("BaseModel.$relationRegistry = new RelationRegistry()") rather than
  mutating a shared one, matching how $adapter and namingStrategy work.

Preloading

  Preloader.processRelation branched on hardcoded 'hasOne' | 'belongsTo',
  so a custom singular relation was handed the raw result array and threw
  '"X.y" cannot reference more than one instance'. It now branches on
  "isMany", which narrows the union at the type level too.

Also: errors moved to the errors module (E_UNSUPPORTED_RELATION_TYPE,
E_DUPLICATE_RELATION_TYPE), createRelationDecorator returns DecoratorFn to
match the other decorators, and the unused ModelRelationOptions added to
types/relations.ts is removed (it collided with the existing export in
types/model.ts).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/types/relations.ts (1)

346-369: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Extend GetRelationModelInstance for singular custom relations.

ModelRelations includes ValidatedCustomOpaqueRelations, but GetRelationModelInstance only selects Relation['instance'] for 'hasOne' | 'belongsTo'. Any custom relation with isMany: false, such as morphTo, is typed as Relation['instance'][]. Carry the isMany discriminator through the opaque type, or extend the singular selection to registered singular custom relations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/relations.ts` around lines 346 - 369, Update
GetRelationModelInstance to recognize registered custom relations with isMany:
false, including singular opaque relations such as morphTo, and return their
relation instance rather than an array. Preserve the existing hasOne/belongsTo
behavior and continue returning arrays for custom relations marked isMany: true,
using the existing opaque relation discriminator.
🧹 Nitpick comments (1)
src/types/model.ts (1)

806-811: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale doc comments after inserting $relationRegistry. In both files, $relationRegistry was inserted between the existing $relationsDefinitions doc comment and its property. Two doc blocks now stack on $relationRegistry, and $relationsDefinitions has no doc comment.

  • src/types/model.ts#L806-L811: move the "A map of defined relationships" comment down to $relationsDefinitions.
  • src/orm/base_model/index.ts#L157-L164: move the "Registered relationships for the given model" comment down to $relationsDefinitions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/model.ts` around lines 806 - 811, Move the existing relationships
doc comment from above $relationRegistry to directly above $relationsDefinitions
in src/types/model.ts lines 806-811 and src/orm/base_model/index.ts lines
157-164. Keep the registry comment attached only to $relationRegistry and
restore the appropriate documentation for $relationsDefinitions in both files.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/types/relations.ts`:
- Around line 346-369: Update GetRelationModelInstance to recognize registered
custom relations with isMany: false, including singular opaque relations such as
morphTo, and return their relation instance rather than an array. Preserve the
existing hasOne/belongsTo behavior and continue returning arrays for custom
relations marked isMany: true, using the existing opaque relation discriminator.

---

Nitpick comments:
In `@src/types/model.ts`:
- Around line 806-811: Move the existing relationships doc comment from above
$relationRegistry to directly above $relationsDefinitions in src/types/model.ts
lines 806-811 and src/orm/base_model/index.ts lines 157-164. Keep the registry
comment attached only to $relationRegistry and restore the appropriate
documentation for $relationsDefinitions in both files.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d8bbc49-d5e3-4d83-8492-d81492dbbbdb

📥 Commits

Reviewing files that changed from the base of the PR and between 4edf3cf and 2c95789.

📒 Files selected for processing (9)
  • src/errors.ts
  • src/orm/base_model/index.ts
  • src/orm/decorators/create_relation_decorator.ts
  • src/orm/preloader/index.ts
  • src/orm/relations/relation_registry.ts
  • src/types/model.ts
  • src/types/relations.ts
  • test/orm/custom_relations.spec.ts
  • test/orm/relation_registry.spec.ts

@thetutlage

thetutlage commented Aug 3, 2026

Copy link
Copy Markdown
Member

I pushed 2c95789 to the branch. Here is what I found and what I changed.

Problems

  1. switch (relation.type) stops working. The contracts you register extend BaseRelationContract, whose type is the whole union. So the member matches every case. That is why the four as any casts appeared in factory_model.ts — your test file's augmentation alone was enough to break narrowing inside Lucid. Any package doing this breaks it for every user.

  2. KnownCustomRelations accepts anything. Putting junk: string in it gives 48 type errors inside src/.

  3. Widening $relationsDefinitions is unsound. setRelated(user, [a, b]) on a hasOne now compiles. It throws at runtime.

  4. Preload is broken for custom singular relations. Preloader.processRelation still checks 'hasOne' | 'belongsTo' by name, so a relation with isMany = false gets the whole array and throws.

  5. unregister() does nothing useful. Booted models hold relation instances, not factories. Your test at line 297 says as much. It only breaks the next model that uses that relation.

Direction

One contract, one registry, no special cases for built-ins.

  • BaseRelationContract is generic over Type and IsMany. Built-ins pass literals ('hasOne', false). Custom relations use the same contract — nothing extra to learn, and 15 duplicated method declarations went away.
  • A custom relation joins the union only if its type matches the key it registered under. Bad entries become never instead of breaking narrowing for everyone.
  • Built-ins are normal registry entries. All five already share one constructor signature, so the switch in $addBuiltInRelation was doing nothing. $addRelation is a single lookup now. $addHasOne and friends stay and delegate to it.
  • Registration is add-only. No unregister, no clear, and built-ins are not privileged. For test isolation, replace the registry instead: BaseModel.$relationRegistry = new RelationRegistry().
  • Preloader branches on isMany.

528 added, 616 removed. 1499 tests pass; the 1 failure is the MySQL check, which also fails on 22.x.

For you

  • The description is out of date — getManyRelationTypes(), RelationFactoryConfig.isMany and unregister are all gone.
  • I kept the ./orm/relations exports since custom relations need them, but let's decide on them before release.
  • Does the new contract still work for the ancestors relation you had in mind?

Comment thread src/orm/main.ts
export * from './decorators/date_time.js'
export { createRelationDecorator } from './decorators/create_relation_decorator.js'
export { RelationRegistry } from './relations/relation_registry.js'
export type { RelationFactory, RelationFactoryConfig } from '../types/relations.js'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These must be exported from the types folder. I avoid missing runtime and types exports from the same module

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.

2 participants