From 926a7e7fdff99637f34db1f85779c13801bc8e8b Mon Sep 17 00:00:00 2001 From: Ross MacPhee Date: Sun, 4 Sep 2022 00:10:51 +0100 Subject: [PATCH 1/2] feat(objection): add model relationship decorators --- docs/tutorials/objection.md | 86 +++++++++++++++++++ .../src/decorators/belongsToOne.spec.ts | 57 ++++++++++++ .../objection/src/decorators/belongsToOne.ts | 13 +++ .../orm/objection/src/decorators/entity.ts | 6 +- .../objection/src/decorators/hasMany.spec.ts | 57 ++++++++++++ .../orm/objection/src/decorators/hasMany.ts | 14 +++ .../objection/src/decorators/hasOne.spec.ts | 57 ++++++++++++ .../orm/objection/src/decorators/hasOne.ts | 13 +++ .../decorators/hasOneThroughRelation.spec.ts | 69 +++++++++++++++ .../src/decorators/hasOneThroughRelation.ts | 14 +++ .../src/decorators/manyToMany.spec.ts | 69 +++++++++++++++ .../objection/src/decorators/manyToMany.ts | 14 +++ .../src/decorators/relatesTo.spec.ts | 31 +++++++ .../orm/objection/src/decorators/relatesTo.ts | 26 ++++++ .../objection/src/domain/RelationshipOpts.ts | 14 +++ packages/orm/objection/src/index.ts | 10 +++ .../orm/objection/src/utils/createJoinKeys.ts | 38 ++++++++ .../src/utils/createRelationshipMapping.ts | 21 +++++ .../src/utils/getJsonEntityRelationships.ts | 16 ++++ 19 files changed, 623 insertions(+), 2 deletions(-) create mode 100644 packages/orm/objection/src/decorators/belongsToOne.spec.ts create mode 100644 packages/orm/objection/src/decorators/belongsToOne.ts create mode 100644 packages/orm/objection/src/decorators/hasMany.spec.ts create mode 100644 packages/orm/objection/src/decorators/hasMany.ts create mode 100644 packages/orm/objection/src/decorators/hasOne.spec.ts create mode 100644 packages/orm/objection/src/decorators/hasOne.ts create mode 100644 packages/orm/objection/src/decorators/hasOneThroughRelation.spec.ts create mode 100644 packages/orm/objection/src/decorators/hasOneThroughRelation.ts create mode 100644 packages/orm/objection/src/decorators/manyToMany.spec.ts create mode 100644 packages/orm/objection/src/decorators/manyToMany.ts create mode 100644 packages/orm/objection/src/decorators/relatesTo.spec.ts create mode 100644 packages/orm/objection/src/decorators/relatesTo.ts create mode 100644 packages/orm/objection/src/domain/RelationshipOpts.ts create mode 100644 packages/orm/objection/src/utils/createJoinKeys.ts create mode 100644 packages/orm/objection/src/utils/createRelationshipMapping.ts create mode 100644 packages/orm/objection/src/utils/getJsonEntityRelationships.ts diff --git a/docs/tutorials/objection.md b/docs/tutorials/objection.md index 9846357f3dd..deae224f218 100644 --- a/docs/tutorials/objection.md +++ b/docs/tutorials/objection.md @@ -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": ".Id", + "to": "." +} +``` + +::: 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": ".", + "to": ".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": ".", + "through": { + "from": "_.Id", + "to": "_.Id" + }, + "to": "." +} +``` + +::: 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 diff --git a/packages/orm/objection/src/decorators/belongsToOne.spec.ts b/packages/orm/objection/src/decorators/belongsToOne.spec.ts new file mode 100644 index 00000000000..156480732ca --- /dev/null +++ b/packages/orm/objection/src/decorators/belongsToOne.spec.ts @@ -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" + } + } + }); + }); +}); diff --git a/packages/orm/objection/src/decorators/belongsToOne.ts b/packages/orm/objection/src/decorators/belongsToOne.ts new file mode 100644 index 00000000000..87dd5d8b0f4 --- /dev/null +++ b/packages/orm/objection/src/decorators/belongsToOne.ts @@ -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); +} diff --git a/packages/orm/objection/src/decorators/entity.ts b/packages/orm/objection/src/decorators/entity.ts index e6ac18c2e69..fd745b224fd 100644 --- a/packages/orm/objection/src/decorators/entity.ts +++ b/packages/orm/objection/src/decorators/entity.ts @@ -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"; /** * @@ -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; }; } diff --git a/packages/orm/objection/src/decorators/hasMany.spec.ts b/packages/orm/objection/src/decorators/hasMany.spec.ts new file mode 100644 index 00000000000..30ff58df47d --- /dev/null +++ b/packages/orm/objection/src/decorators/hasMany.spec.ts @@ -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" + } + } + }); + }); +}); diff --git a/packages/orm/objection/src/decorators/hasMany.ts b/packages/orm/objection/src/decorators/hasMany.ts new file mode 100644 index 00000000000..650a00709d1 --- /dev/null +++ b/packages/orm/objection/src/decorators/hasMany.ts @@ -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}); +} diff --git a/packages/orm/objection/src/decorators/hasOne.spec.ts b/packages/orm/objection/src/decorators/hasOne.spec.ts new file mode 100644 index 00000000000..1d096567696 --- /dev/null +++ b/packages/orm/objection/src/decorators/hasOne.spec.ts @@ -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" + } + } + }); + }); +}); diff --git a/packages/orm/objection/src/decorators/hasOne.ts b/packages/orm/objection/src/decorators/hasOne.ts new file mode 100644 index 00000000000..4af4e258ee9 --- /dev/null +++ b/packages/orm/objection/src/decorators/hasOne.ts @@ -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); +} diff --git a/packages/orm/objection/src/decorators/hasOneThroughRelation.spec.ts b/packages/orm/objection/src/decorators/hasOneThroughRelation.spec.ts new file mode 100644 index 00000000000..f64f0aa681e --- /dev/null +++ b/packages/orm/objection/src/decorators/hasOneThroughRelation.spec.ts @@ -0,0 +1,69 @@ +import {Entity, HasOneThroughRelation, IdColumn} from "@tsed/objection"; + +import {Model} from "objection"; + +describe("@HasOneThroughRelation", () => { + it("should set metadata", () => { + @Entity("pet") + class Pet extends Model { + @IdColumn() + id!: string; + userId?: string; + } + @Entity("user") + class User extends Model { + @IdColumn() + id!: string; + @HasOneThroughRelation() + pet?: Pet; + } + expect(User.relationMappings).toEqual({ + pet: { + relation: Model.HasOneThroughRelation, + modelClass: Pet, + join: { + from: "user.id", + through: { + from: "user_pet.userId", + to: "user_pet.petId" + }, + to: "pet.id" + } + } + }); + }); + it("should set custom relationship path", () => { + @Entity("pet") + class Pet extends Model { + @IdColumn() + id!: string; + petId?: string; + } + @Entity("user") + class User extends Model { + @IdColumn() + id!: string; + userId!: string; + @HasOneThroughRelation({ + from: "userId", + through: {from: "user_pet.ownerId", to: "user_pet.petId"}, + to: "petId" + }) + pet?: Pet; + } + expect(User.relationMappings).toEqual({ + pet: { + relation: Model.HasOneThroughRelation, + modelClass: Pet, + join: { + from: "user.userId", + through: { + from: "user_pet.ownerId", + to: "user_pet.petId" + }, + to: "pet.petId" + } + } + }); + }); +}); diff --git a/packages/orm/objection/src/decorators/hasOneThroughRelation.ts b/packages/orm/objection/src/decorators/hasOneThroughRelation.ts new file mode 100644 index 00000000000..a681b279917 --- /dev/null +++ b/packages/orm/objection/src/decorators/hasOneThroughRelation.ts @@ -0,0 +1,14 @@ +import {RelationshipOptsWithThrough, RelationshipOptsWithoutThrough} from "../domain/RelationshipOpts"; + +import {Model} from "objection"; +import {RelatesTo} from "./relatesTo"; + +/** + * + * @param opts + * @decorator + * @objection + */ +export function HasOneThroughRelation(opts?: RelationshipOptsWithThrough): PropertyDecorator { + return RelatesTo(Model.HasOneThroughRelation, opts); +} diff --git a/packages/orm/objection/src/decorators/manyToMany.spec.ts b/packages/orm/objection/src/decorators/manyToMany.spec.ts new file mode 100644 index 00000000000..b8716b168db --- /dev/null +++ b/packages/orm/objection/src/decorators/manyToMany.spec.ts @@ -0,0 +1,69 @@ +import {Entity, IdColumn, ManyToMany} from "@tsed/objection"; + +import {Model} from "objection"; + +describe("@ManyToMany", () => { + it("should set metadata", () => { + @Entity("pet") + class Pet extends Model { + @IdColumn() + id!: string; + userId?: string; + } + @Entity("user") + class User extends Model { + @IdColumn() + id!: string; + @ManyToMany(Pet) + pets?: Pet[]; + } + expect(User.relationMappings).toEqual({ + pets: { + relation: Model.ManyToManyRelation, + modelClass: Pet, + join: { + from: "user.id", + through: { + from: "user_pet.userId", + to: "user_pet.petId" + }, + to: "pet.id" + } + } + }); + }); + it("should set custom relationship path", () => { + @Entity("pet") + class Pet extends Model { + @IdColumn() + id!: string; + petId?: string; + } + @Entity("user") + class User extends Model { + @IdColumn() + id!: string; + userId!: string; + @ManyToMany(Pet, { + from: "userId", + through: {from: "user_pet.ownerId", to: "user_pet.petId"}, + to: "petId" + }) + pets?: Pet[]; + } + expect(User.relationMappings).toEqual({ + pets: { + relation: Model.ManyToManyRelation, + modelClass: Pet, + join: { + from: "user.userId", + through: { + from: "user_pet.ownerId", + to: "user_pet.petId" + }, + to: "pet.petId" + } + } + }); + }); +}); diff --git a/packages/orm/objection/src/decorators/manyToMany.ts b/packages/orm/objection/src/decorators/manyToMany.ts new file mode 100644 index 00000000000..641573d79c2 --- /dev/null +++ b/packages/orm/objection/src/decorators/manyToMany.ts @@ -0,0 +1,14 @@ +import {Model, ModelClassSpecifier} from "objection"; + +import {RelatesTo} from "./relatesTo"; +import {RelationshipOptsWithThrough} from "../domain/RelationshipOpts"; + +/** + * + * @param opts + * @decorator + * @objection + */ +export function ManyToMany(type: ModelClassSpecifier, opts?: RelationshipOptsWithThrough): PropertyDecorator { + return RelatesTo(Model.ManyToManyRelation, {...opts, type}); +} diff --git a/packages/orm/objection/src/decorators/relatesTo.spec.ts b/packages/orm/objection/src/decorators/relatesTo.spec.ts new file mode 100644 index 00000000000..c718d26f00b --- /dev/null +++ b/packages/orm/objection/src/decorators/relatesTo.spec.ts @@ -0,0 +1,31 @@ +import {Entity, IdColumn, RelatesTo} from "@tsed/objection"; + +import {Model} from "objection"; + +describe("@RelatesTo", () => { + it("should set metadata", () => { + @Entity("user") + class User extends Model { + @IdColumn() + id!: string; + } + @Entity("movie") + class Movie extends Model { + @IdColumn() + id!: string; + userId!: string; + @RelatesTo(Model.BelongsToOneRelation) + user?: User; + } + expect(Movie.relationMappings).toEqual({ + user: { + relation: Model.BelongsToOneRelation, + modelClass: User, + join: { + from: "movie.userId", + to: "user.id" + } + } + }); + }); +}); diff --git a/packages/orm/objection/src/decorators/relatesTo.ts b/packages/orm/objection/src/decorators/relatesTo.ts new file mode 100644 index 00000000000..2586b352d5f --- /dev/null +++ b/packages/orm/objection/src/decorators/relatesTo.ts @@ -0,0 +1,26 @@ +import {CollectionOf, Property} from "@tsed/schema"; +import {RelationshipOpts, isModelClassFactory} from "../domain/RelationshipOpts"; +import {StoreFn, useDecorators} from "@tsed/core"; + +import {OBJECTION_RELATIONSHIP_KEY} from "../utils/getJsonEntityRelationships"; +import {RelationType} from "objection"; +import {createRelationshipMapping} from "../utils/createRelationshipMapping"; + +/** + * + * @param relation + * @param opts + * @decorator + * @objection + */ +export function RelatesTo(relation: RelationType, opts?: RelationshipOpts): PropertyDecorator { + return useDecorators( + opts?.type ? CollectionOf(opts.type) : Property(), + StoreFn((store, params) => { + if (opts && isModelClassFactory(opts.type)) { + opts.type = opts.type(); + } + store.set(OBJECTION_RELATIONSHIP_KEY, createRelationshipMapping(params, relation, opts)); + }) + ); +} diff --git a/packages/orm/objection/src/domain/RelationshipOpts.ts b/packages/orm/objection/src/domain/RelationshipOpts.ts new file mode 100644 index 00000000000..9a24cb85954 --- /dev/null +++ b/packages/orm/objection/src/domain/RelationshipOpts.ts @@ -0,0 +1,14 @@ +import {isFunction, isString} from "@tsed/core"; +import {ModelClassFactory, ModelClassSpecifier, RelationJoin, RelationMapping} from "objection"; + +export type RelationshipOptsWithThrough = Partial & Omit, "relation" | "join" | "modelClass">; + +export type RelationshipOptsWithoutThrough = Omit; + +export type RelationshipOpts = (RelationshipOptsWithThrough | RelationshipOptsWithoutThrough) & {type?: ModelClassSpecifier}; + +export const isRelationshipOptsWithThrough = (opts?: RelationshipOpts): opts is RelationshipOptsWithThrough => + opts !== undefined && (opts).through !== undefined; + +export const isModelClassFactory = (type?: ModelClassSpecifier): type is ModelClassFactory => + type !== undefined && !isString(type) && isFunction(type) && !("prototype" in type); diff --git a/packages/orm/objection/src/index.ts b/packages/orm/objection/src/index.ts index 93be17bb4bd..145bd6057b5 100644 --- a/packages/orm/objection/src/index.ts +++ b/packages/orm/objection/src/index.ts @@ -6,15 +6,25 @@ export * from "./components/createBooleanColumn"; export * from "./components/createIdColumn"; export * from "./components/createNumberColumn"; export * from "./components/createStringColumn"; +export * from "./decorators/belongsToOne"; export * from "./decorators/columnOptions"; export * from "./decorators/decimal"; export * from "./decorators/entity"; +export * from "./decorators/hasMany"; +export * from "./decorators/hasOne"; +export * from "./decorators/hasOneThroughRelation"; export * from "./decorators/idColumn"; +export * from "./decorators/manyToMany"; +export * from "./decorators/relatesTo"; export * from "./domain/ColumnOpts"; +export * from "./domain/RelationshipOpts"; export * from "./domain/interfaces"; export * from "./services/ColumnTypesContainer"; export * from "./services/ObjectionConnection"; export * from "./utils/connect"; export * from "./utils/createColumns"; +export * from "./utils/createJoinKeys"; +export * from "./utils/createRelationshipMapping"; export * from "./utils/defineStaticGetter"; export * from "./utils/getColumnCtx"; +export * from "./utils/getJsonEntityRelationships"; diff --git a/packages/orm/objection/src/utils/createJoinKeys.ts b/packages/orm/objection/src/utils/createJoinKeys.ts new file mode 100644 index 00000000000..c33881b7c53 --- /dev/null +++ b/packages/orm/objection/src/utils/createJoinKeys.ts @@ -0,0 +1,38 @@ +import {Model, RelationType} from "objection"; +import {RelationshipOpts, isRelationshipOptsWithThrough} from "../domain/RelationshipOpts"; + +import {Metadata} from "@tsed/core"; + +/** + * @ignore + */ +export function createJoinKeys(targetClass: any, targetSuper: any, propertyKey: string, relation: RelationType, opts?: RelationshipOpts) { + const FOREIGN_MODEL = opts?.type || Metadata.getType(targetSuper, propertyKey); + const SOURCE_MODEL_KEY = `${targetClass.tableName}.${opts?.from || targetClass.idColumn}`; + const FOREIGN_MODEL_KEY = `${FOREIGN_MODEL.tableName}.${opts?.to || FOREIGN_MODEL.idColumn}`; + + switch (relation) { + case Model.HasManyRelation: + case Model.HasOneRelation: + return { + from: SOURCE_MODEL_KEY, + to: `${FOREIGN_MODEL.tableName}.${opts?.to || targetClass.tableName + "Id"}` + }; + case Model.ManyToManyRelation: + case Model.HasOneThroughRelation: + return { + from: SOURCE_MODEL_KEY, + through: (isRelationshipOptsWithThrough(opts) && opts.through) || { + from: `${targetClass.tableName}_${FOREIGN_MODEL.tableName}.${targetClass.tableName}Id`, + to: `${targetClass.tableName}_${FOREIGN_MODEL.tableName}.${FOREIGN_MODEL.tableName}Id` + }, + to: FOREIGN_MODEL_KEY + }; + case Model.BelongsToOneRelation: + default: + return { + from: `${targetClass.tableName}.${opts?.from || propertyKey + "Id"}`, + to: FOREIGN_MODEL_KEY + }; + } +} diff --git a/packages/orm/objection/src/utils/createRelationshipMapping.ts b/packages/orm/objection/src/utils/createRelationshipMapping.ts new file mode 100644 index 00000000000..23f1a0802b8 --- /dev/null +++ b/packages/orm/objection/src/utils/createRelationshipMapping.ts @@ -0,0 +1,21 @@ +import {DecoratorParameters, Metadata, getClass} from "@tsed/core"; + +import {RelationType} from "objection"; +import {RelationshipOpts} from "../domain/RelationshipOpts"; +import {createJoinKeys} from "./createJoinKeys"; + +/** + * @ignore + */ +export function createRelationshipMapping([target, propertyKey]: DecoratorParameters, relation: RelationType, opts?: RelationshipOpts) { + return (targetClass: any) => ({ + [propertyKey]: { + relation, + modelClass: opts?.type || Metadata.getType(target, propertyKey), + join: createJoinKeys(targetClass, target, String(propertyKey), relation, opts), + modify: opts?.modify, + filter: opts?.filter, + beforeInsert: opts?.beforeInsert + } + }); +} diff --git a/packages/orm/objection/src/utils/getJsonEntityRelationships.ts b/packages/orm/objection/src/utils/getJsonEntityRelationships.ts new file mode 100644 index 00000000000..a9efba5f4a7 --- /dev/null +++ b/packages/orm/objection/src/utils/getJsonEntityRelationships.ts @@ -0,0 +1,16 @@ +import {getColumns} from "./createColumns"; + +/** + * @ignore + */ +export const OBJECTION_RELATIONSHIP_KEY = "objectionRelationship"; + +/** + * @ignore + */ +export function getJsonEntityRelationships(target: any) { + return getColumns(target) + .filter((col) => col.store.get(OBJECTION_RELATIONSHIP_KEY)) + .map((col) => col.store.get(OBJECTION_RELATIONSHIP_KEY)(target)) + .reduce((dict, col) => ({...dict, ...col}), {}); +} From c5e8a652cdeab0846d2f72aa3be719cd9270ef79 Mon Sep 17 00:00:00 2001 From: Ross MacPhee Date: Thu, 8 Sep 2022 00:54:26 +0100 Subject: [PATCH 2/2] docs(objection): add docs sidebar link --- docs/.vuepress/config.base.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/.vuepress/config.base.js b/docs/.vuepress/config.base.js index b2b5a626e76..7d8d69e4a71 100644 --- a/docs/.vuepress/config.base.js +++ b/docs/.vuepress/config.base.js @@ -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)) }, {