Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/factories/factory_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,16 @@ export class FactoryModel<Model extends LucidModel> implements FactoryModelContr

switch (modelRelation.type) {
case 'belongsTo':
this.relations[relation as string] = new BelongsTo(modelRelation, callback as any)
this.relations[relation as string] = new BelongsTo(modelRelation as any, callback as any)
break
case 'hasOne':
this.relations[relation as string] = new HasOne(modelRelation, callback as any)
this.relations[relation as string] = new HasOne(modelRelation as any, callback as any)
break
case 'hasMany':
this.relations[relation as string] = new HasMany(modelRelation, callback as any)
this.relations[relation as string] = new HasMany(modelRelation as any, callback as any)
break
case 'manyToMany':
this.relations[relation as string] = new ManyToMany(modelRelation, callback as any)
this.relations[relation as string] = new ManyToMany(modelRelation as any, callback as any)
break
case 'hasManyThrough':
throw new Error(
Expand Down
54 changes: 42 additions & 12 deletions src/orm/base_model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
type ModelRelations,
type RelationOptions,
type RelationshipsContract,
type BaseRelationContract,
type ThroughRelationOptions,
type ManyToManyRelationOptions,
} from '../../types/relations.js'
Expand All @@ -59,6 +60,7 @@ import { HasMany } from '../relations/has_many/index.js'
import { BelongsTo } from '../relations/belongs_to/index.js'
import { ManyToMany } from '../relations/many_to_many/index.js'
import { HasManyThrough } from '../relations/has_many_through/index.js'
import { RelationRegistry } from '../relations/relation_registry.js'
import { CamelCaseNamingStrategy } from '../naming_strategies/camel_case.js'
import { LazyLoadAggregates } from '../relations/aggregates_loader/lazy_load.js'
import {
Expand All @@ -71,7 +73,6 @@ import {
compareValues,
} from '../../utils/index.js'

const MANY_RELATIONS = ['hasMany', 'manyToMany', 'hasManyThrough']
const DATE_TIME_TYPES = {
date: 'date',
datetime: 'datetime',
Expand Down Expand Up @@ -158,7 +159,7 @@ class BaseModelImpl implements LucidRow {
/**
* Registered relationships for the given model
*/
static $relationsDefinitions: Map<string, RelationshipsContract>
static $relationsDefinitions: Map<string, BaseRelationContract<LucidModel, LucidModel>>

/**
* The name of database table. It is auto generated from the model name, unless
Expand Down Expand Up @@ -543,24 +544,53 @@ class BaseModelImpl implements LucidRow {
type: ModelRelations<LucidModel, LucidModel>['__opaque_type'],
relatedModel: () => LucidModel,
options: ModelRelationOptions
) {
): BaseRelationContract<LucidModel, LucidModel> {
// Try built-in relations first
const builtInRelation = this.$addBuiltInRelation(name, type, relatedModel, options)
if (builtInRelation) {
return builtInRelation
}

// Check for custom relations in the registry
const factory = RelationRegistry.get(type as string)
if (!factory) {
throw new Error(
`"${type}" is not a supported relation type. Did you forget to register it with RelationRegistry.register()?`
)
}

// Create the custom relation
const relation = factory.create(name, relatedModel, options, this)
this.$relationsDefinitions.set(name, relation)
return relation
}

/**
* Handles built-in relation types
*/
private static $addBuiltInRelation(
name: string,
type: string,
relatedModel: () => LucidModel,
options: ModelRelationOptions
): BaseRelationContract<LucidModel, LucidModel> | null {
switch (type) {
case 'hasOne':
this.$addHasOne(name, relatedModel, options)
break
return this.$relationsDefinitions.get(name)!
case 'hasMany':
this.$addHasMany(name, relatedModel, options)
break
return this.$relationsDefinitions.get(name)!
case 'belongsTo':
this.$addBelongsTo(name, relatedModel, options)
break
return this.$relationsDefinitions.get(name)!
case 'manyToMany':
this.$addManyToMany(
name,
relatedModel,
options as ManyToManyRelationOptions<ModelRelations<LucidModel, LucidModel>>
)
break
return this.$relationsDefinitions.get(name)!
case 'hasManyThrough':
this.$addHasManyThrough(
name,
Expand All @@ -571,9 +601,9 @@ class BaseModelImpl implements LucidRow {
ModelRelations<LucidModel, LucidModel>
>
)
break
return this.$relationsDefinitions.get(name)!
default:
throw new Error(`${type} is not a supported relation type`)
return null
}
}

Expand Down Expand Up @@ -695,7 +725,7 @@ class BaseModelImpl implements LucidRow {
* Define relationships
*/
this.$defineProperty('$relationsDefinitions', new Map(), (value) => {
const relations = new Map<string, RelationshipsContract>()
const relations = new Map<string, BaseRelationContract<LucidModel, LucidModel>>()
value.forEach((relation, key) => {
const relationClone = relation.clone(this)
relations.set(key, relationClone)
Expand Down Expand Up @@ -1721,7 +1751,7 @@ class BaseModelImpl implements LucidRow {
/**
* Reset array before invoking $pushRelated
*/
if (MANY_RELATIONS.includes(relation.type)) {
if (relation.isMany) {
if (!Array.isArray(models)) {
throw new Exception(
`"${Model.name}.${key}" must be an array when setting "${relation.type}" relationship`
Expand Down Expand Up @@ -1750,7 +1780,7 @@ class BaseModelImpl implements LucidRow {
/**
* Create multiple for `hasMany` `manyToMany` and `hasManyThrough`
*/
if (MANY_RELATIONS.includes(relation.type)) {
if (relation.isMany) {
this.$preloaded[key] = ((this.$preloaded[key] || []) as LucidRow[]).concat(models)
return
}
Expand Down
50 changes: 50 additions & 0 deletions src/orm/decorators/create_relation_decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import type { LucidModel } from '../../types/model.js'
import type { ModelRelationTypes } from '../../types/relations.js'

/**
* Utility to create a custom relation decorator.
* Useful for third-party packages that want to add custom relation types.
*
* @example
* ```ts
* // In your package
* export const myRelation: MyRelationDecoratorType = createRelationDecorator(
* 'myRelation'
* )
*
* // Usage
* class SomeModel extends BaseModel {
* @myRelation(() => SomeModel, { ...someOptions })
* declare someRelation: MyRelation<typeof SomeModel>
* }
* ```
*/
export function createRelationDecorator<
TRelationType extends ModelRelationTypes['__opaque_type'] = ModelRelationTypes['__opaque_type'],
TOptions = any,
>(
relationType: TRelationType
): (relatedModel: () => LucidModel, options?: TOptions) => PropertyDecorator {
return function decorator(relatedModel, options?) {
return function decorateAsRelation(target, property: string | symbol) {
const Model = target.constructor as LucidModel
Model.boot()
const propertyName = typeof property === 'symbol' ? property.toString() : property
Model.$addRelation(
propertyName,
relationType,
relatedModel,
Object.assign({ relatedModel }, options) as any
)
}
}
}
3 changes: 3 additions & 0 deletions src/orm/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
export * from './decorators/index.js'
export * from './decorators/date.js'
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'
export { BaseModel, scope } from './base_model/index.js'
export { ModelQueryBuilder } from './query_builder/index.js'
export { ModelPaginator } from './paginator/index.js'
Expand Down
1 change: 1 addition & 0 deletions src/orm/relations/belongs_to/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class BelongsTo implements BelongsToRelationContract<LucidModel, LucidMod
* Relationship name
*/
readonly type = 'belongsTo'
readonly isMany = false

/**
* Whether or not the relationship instance has been booted
Expand Down
1 change: 1 addition & 0 deletions src/orm/relations/has_many/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class HasMany implements HasManyRelationContract<LucidModel, LucidModel>
* The relationship name
*/
readonly type = 'hasMany'
readonly isMany = true

/**
* Whether or not the relationship instance has been
Expand Down
1 change: 1 addition & 0 deletions src/orm/relations/has_many_through/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ensureRelationIsBooted } from '../../../utils/index.js'
*/
export class HasManyThrough implements HasManyThroughRelationContract<LucidModel, LucidModel> {
type = 'hasManyThrough' as const
readonly isMany = true

booted: boolean = false
serializeAs
Expand Down
1 change: 1 addition & 0 deletions src/orm/relations/has_one/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ensureRelationIsBooted, getValue } from '../../../utils/index.js'
*/
export class HasOne implements HasOneRelationContract<LucidModel, LucidModel> {
readonly type = 'hasOne'
readonly isMany = false

booted: boolean = false
serializeAs
Expand Down
3 changes: 3 additions & 0 deletions src/orm/relations/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export { BelongsToQueryClient } from './belongs_to/query_client.js'
export { HasManyQueryClient } from './has_many/query_client.js'
export { HasOneQueryClient } from './has_one/query_client.js'
export { ManyToManyQueryClient } from './many_to_many/query_client.js'
export { BaseQueryBuilder as RelationBaseQueryBuilder } from './base/query_builder.js'
export { BaseSubQueryBuilder as RelationBaseSubQueryBuilder } from './base/sub_query_builder.js'
export { KeysExtractor } from './keys_extractor.js'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
1 change: 1 addition & 0 deletions src/orm/relations/many_to_many/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ensureRelationIsBooted, getValue } from '../../../utils/index.js'
*/
export class ManyToMany implements ManyToManyRelationContract<LucidModel, LucidModel> {
type = 'manyToMany' as const
readonly isMany = true

booted: boolean = false
serializeAs
Expand Down
82 changes: 82 additions & 0 deletions src/orm/relations/relation_registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import type { RelationFactory, RelationFactoryConfig } from '../../types/relations.js'

/**
* Registry for managing custom relation types.
* Allows third-party packages to register new relation types.
*/
export class RelationRegistry {
private static relations = new Map<string, RelationFactory>()

/**
* Registers a new relation type
*
* @example
* ```ts
* RelationRegistry.register('myRelation', {
* create(relationName, relatedModel, options, model) {
* return new MyRelation(relationName, relatedModel, options, model)
* }
* })
* ```
*/
static register(type: string, factoryConfig: RelationFactoryConfig): void {
const BUILT_IN_TYPES = ['hasOne', 'hasMany', 'belongsTo', 'manyToMany', 'hasManyThrough']

if (BUILT_IN_TYPES.includes(type)) {
throw new Error(
`Cannot register "${type}": it is a built-in relation type. ` +
`Built-in types (${BUILT_IN_TYPES.join(', ')}) cannot be overridden. ` +
`Please use a different name for your custom relation.`
)
}

if (this.relations.has(type)) {
throw new Error(`Relation type "${type}" is already registered`)
}

// Inject the type into the factory config
const factory: RelationFactory = {
...factoryConfig,
type,
}

this.relations.set(type, factory)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Retrieves a relation factory by type
*/
static get(type: string): RelationFactory | undefined {
return this.relations.get(type)
}

/**
* Checks if a relation type is registered
*/
static has(type: string): boolean {
return this.relations.has(type)
}

/**
* Unregisters a relation type (mainly for testing)
*/
static unregister(type: string): void {
this.relations.delete(type)
}

/**
* Clears all registered relations (mainly for testing)
*/
static clear(): void {
this.relations.clear()
}
}
3 changes: 2 additions & 1 deletion src/types/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
type PreloaderContract,
type RelationOptions,
type RelationshipsContract,
type BaseRelationContract,
type ThroughRelationOptions,
type WhereHas,
type WithAggregate,
Expand Down Expand Up @@ -802,7 +803,7 @@ export interface LucidModel {
/**
* A map of defined relationships
*/
$relationsDefinitions: Map<string, RelationshipsContract>
$relationsDefinitions: Map<string, BaseRelationContract<LucidModel, LucidModel>>

/**
* A map of computed properties
Expand Down
Loading