Skip to content

Commit

Permalink
feat(objection): add model relationship decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
Ross MacPhee committed Sep 7, 2022
1 parent e907869 commit 926a7e7
Show file tree
Hide file tree
Showing 19 changed files with 623 additions and 2 deletions.
86 changes: 86 additions & 0 deletions docs/tutorials/objection.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,92 @@ export class User extends Model {
}
```

## Relationships

Ts.ED enables you to define relationships between models on properties directly, using decorators such as @@BelongsToOne@@, @@HasMany@@, @@HasOne@@, @@HasOneThroughRelationship@@, @@ManyToMany@@ or @@RelatesTo@@.

You can supply a configuration object via (@@RelationshipOpts@@) into the decorator factor to override the default join keys and configure a relationship like you normally would via `relationMappings`. For collection-type relationships, you must also specify the model you wish to use and we will also apply the @@CollectionOf@@ decorator for you automatically.

This expressive usage ensures that your domain models are correctly typed for usage alongside [Objection.js's Graph API](https://vincit.github.io/objection.js/api/query-builder/eager-methods.html).

```typescript
/**
* All work in a similar manner:
* - @HasMany, @HasOne, @HasOneThroughRelation, @ManyToMany, @RelatesTo
*/
import {Entity, BelongsToOne} from "@tsed/objection";

@Entity("user")
class User extends Model {
@IdColumn()
id!: string;
}

@Entity("movie")
class Movie extends Model {
@IdColumn()
id!: string;

ownerId!: string;

@BelongsToOne()
owner?: User;
}

// Retrieve the related user
const owner = await Movie.relatedQuery("owner").for(1);

// Retrieve the movie with their owner
const movie = await Movie.query().for(1).withGraphFetched("owner");
```

### Default joining keys

When used in conjunction with @@Entity@@ and @@IdColumn@@, Ts.ED attempts to provide you with a sensible default for your join keys out of the box, reducing the amount of boilerplate you need to write.

In the instance of @@BelongsToOne@@, the default join keys will be:

```json
{
"from": "<sourceModelTable>.<foreignModelProperty>Id",
"to": "<foreignModelTable>.<foreignModelIdColumn>"
}
```

::: tip
An example of the keys outputted above could be `movie.ownerId` and `user.id` respectively.
:::

In the instances of @@HasMany@@ and @@HasOne@@, the default join keys will be:

```json
{
"from": "<sourceModelTable>.<sourceModelIdColumn>",
"to": "<foreignModelTable>.<sourceModelTable>Id"
}
```

::: tip
An example of the keys outputted above could be `user.id` and `authentication.userId` respectively.
:::

In the instances of @@ManyToMany@@ and @@HasOneThroughRelation@@, the default join key will be:

```json
{
"from": "<sourceModelTable>.<sourceModelIdColumn>",
"through": {
"from": "<sourceModelTable>_<foreignModelTable>.<sourceModelTable>Id",
"to": "<sourceModelTable>_<foreignModelTable>.<foreignModelTable>Id"
},
"to": "<foreignModelTable>.<foreignModelIdColumn>"
}
```

::: tip
An example of the keys outputted above could be `user.id`, `user_authentication.userId`, `user_authentication.authenticationId` and `authentication.id` respectively.
:::

## Get connection

```typescript
Expand Down
57 changes: 57 additions & 0 deletions packages/orm/objection/src/decorators/belongsToOne.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {BelongsToOne, Entity, IdColumn} from "@tsed/objection";

import {Model} from "objection";

describe("@BelongsToOne", () => {
it("should set metadata", () => {
@Entity("user")
class User extends Model {
@IdColumn()
id!: string;
}
@Entity("movie")
class Movie extends Model {
@IdColumn()
id!: string;
userId!: string;
@BelongsToOne()
user?: User;
}
expect(Movie.relationMappings).toEqual({
user: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: "movie.userId",
to: "user.id"
}
}
});
});
it("should set custom relationship path", () => {
@Entity("user")
class User extends Model {
@IdColumn()
id!: string;
userId!: string;
}
@Entity("movie")
class Movie extends Model {
@IdColumn()
id!: string;
ownerId!: string;
@BelongsToOne({from: "ownerId", to: "userId"})
user?: User;
}
expect(Movie.relationMappings).toEqual({
user: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: "movie.ownerId",
to: "user.userId"
}
}
});
});
});
13 changes: 13 additions & 0 deletions packages/orm/objection/src/decorators/belongsToOne.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Model} from "objection";
import {RelatesTo} from "./relatesTo";
import {RelationshipOptsWithoutThrough} from "../domain/RelationshipOpts";

/**
*
* @param opts
* @decorator
* @objection
*/
export function BelongsToOne(opts?: RelationshipOptsWithoutThrough): PropertyDecorator {
return RelatesTo(Model.BelongsToOneRelation, opts);
}
6 changes: 4 additions & 2 deletions packages/orm/objection/src/decorators/entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Type} from "@tsed/core";
import {getJsonSchema} from "@tsed/schema";
import {defineStaticGetter} from "../utils/defineStaticGetter";
import {getJsonEntityRelationships} from "../utils/getJsonEntityRelationships";
import {getJsonSchema} from "@tsed/schema";

/**
*
Expand All @@ -14,9 +15,10 @@ export function Entity(tableName: string): ClassDecorator {
}

return (target: any) => {
const originalRelationMappings = target["relationMappings"];
defineStaticGetter(target, "tableName", () => tableName);
defineStaticGetter(target, "jsonSchema", () => getJsonSchema(target));

defineStaticGetter(target, "relationMappings", () => originalRelationMappings || getJsonEntityRelationships(target));
return target;
};
}
Expand Down
57 changes: 57 additions & 0 deletions packages/orm/objection/src/decorators/hasMany.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {Entity, HasMany, IdColumn} from "@tsed/objection";

import {Model} from "objection";

describe("@HasMany", () => {
it("should set metadata", () => {
@Entity("pet")
class Pet extends Model {
@IdColumn()
id!: string;
userId?: string;
}
@Entity("user")
class User extends Model {
@IdColumn()
id!: string;
@HasMany(Pet)
pets?: Pet[];
}
expect(User.relationMappings).toEqual({
pets: {
relation: Model.HasManyRelation,
modelClass: Pet,
join: {
from: "user.id",
to: "pet.userId"
}
}
});
});
it("should set custom relationship path", () => {
@Entity("pet")
class Pet extends Model {
@IdColumn()
id!: string;
ownerId?: string;
}
@Entity("user")
class User extends Model {
@IdColumn()
id!: string;
userId!: string;
@HasMany(Pet, {from: "userId", to: "ownerId"})
pets?: Pet[];
}
expect(User.relationMappings).toEqual({
pets: {
relation: Model.HasManyRelation,
modelClass: Pet,
join: {
from: "user.userId",
to: "pet.ownerId"
}
}
});
});
});
14 changes: 14 additions & 0 deletions packages/orm/objection/src/decorators/hasMany.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Model, ModelClassSpecifier} from "objection";

import {RelatesTo} from "./relatesTo";
import {RelationshipOptsWithThrough} from "../domain/RelationshipOpts";

/**
*
* @param opts
* @decorator
* @objection
*/
export function HasMany(type: ModelClassSpecifier, opts?: RelationshipOptsWithThrough): PropertyDecorator {
return RelatesTo(Model.HasManyRelation, {...opts, type});
}
57 changes: 57 additions & 0 deletions packages/orm/objection/src/decorators/hasOne.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {Entity, HasOne, IdColumn} from "@tsed/objection";

import {Model} from "objection";

describe("@HasOne", () => {
it("should set metadata", () => {
@Entity("pet")
class Pet extends Model {
@IdColumn()
id!: string;
userId?: string;
}
@Entity("user")
class User extends Model {
@IdColumn()
id!: string;
@HasOne()
pet?: Pet;
}
expect(User.relationMappings).toEqual({
pet: {
relation: Model.HasOneRelation,
modelClass: Pet,
join: {
from: "user.id",
to: "pet.userId"
}
}
});
});
it("should set custom relationship path", () => {
@Entity("pet")
class Pet extends Model {
@IdColumn()
id!: string;
ownerId?: string;
}
@Entity("user")
class User extends Model {
@IdColumn()
id!: string;
userId!: string;
@HasOne({from: "userId", to: "ownerId"})
pet?: Pet;
}
expect(User.relationMappings).toEqual({
pet: {
relation: Model.HasOneRelation,
modelClass: Pet,
join: {
from: "user.userId",
to: "pet.ownerId"
}
}
});
});
});
13 changes: 13 additions & 0 deletions packages/orm/objection/src/decorators/hasOne.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Model} from "objection";
import {RelatesTo} from "./relatesTo";
import {RelationshipOptsWithoutThrough} from "../domain/RelationshipOpts";

/**
*
* @param opts
* @decorator
* @objection
*/
export function HasOne(opts?: RelationshipOptsWithoutThrough): PropertyDecorator {
return RelatesTo(Model.HasOneRelation, opts);
}
Loading

0 comments on commit 926a7e7

Please sign in to comment.