Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(objection): add model relationship decorators #2078

Merged
Merged
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
3 changes: 2 additions & 1 deletion docs/.vuepress/config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,8 @@ module.exports = ({title, description, base = "", url, apiRedirectUrl = "", them
{title: "Agenda", path: base + "/tutorials/agenda"},
{title: "Terminus", path: base + "/tutorials/terminus"},
{title: "Serverless", path: base + "/tutorials/serverless"},
{title: "IORedis", path: base + "/tutorials/ioredis"}
{title: "IORedis", path: base + "/tutorials/ioredis"},
{title: "Objection.js", path: base + "/tutorials/objection"}
].sort((a, b) => (a.title < b.title ? -1 : 1))
},
{
Expand Down
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