diff --git a/integration/crud-typeorm/projects/index.ts b/integration/crud-typeorm/projects/index.ts index 3952fdb0..3968b4eb 100644 --- a/integration/crud-typeorm/projects/index.ts +++ b/integration/crud-typeorm/projects/index.ts @@ -1,2 +1,3 @@ export * from './project.entity'; +export * from './user-project.entity'; export * from './projects.service'; diff --git a/integration/crud-typeorm/projects/project.entity.ts b/integration/crud-typeorm/projects/project.entity.ts index 9a9e2af0..8d1f111b 100644 --- a/integration/crud-typeorm/projects/project.entity.ts +++ b/integration/crud-typeorm/projects/project.entity.ts @@ -1,4 +1,4 @@ -import { Entity, Column, ManyToOne, ManyToMany, JoinTable } from 'typeorm'; +import { Entity, Column, ManyToOne, ManyToMany, JoinTable, OneToMany } from 'typeorm'; import { IsOptional, IsString, @@ -12,6 +12,7 @@ import { CrudValidationGroups } from '@nestjsx/crud'; import { BaseEntity } from '../base-entity'; import { Company } from '../companies/company.entity'; import { User } from '../users/user.entity'; +import { UserProject } from './user-project.entity'; const { CREATE, UPDATE } = CrudValidationGroups; @@ -46,6 +47,22 @@ export class Project extends BaseEntity { company?: Company; @ManyToMany((type) => User, (u) => u.projects, { cascade: true }) - @JoinTable() + @JoinTable({ + name: 'user_projects', + joinColumn: { + name: 'projectId', + referencedColumnName: 'id', + }, + inverseJoinColumn: { + name: 'userId', + referencedColumnName: 'id', + }, + }) users?: User[]; + + @OneToMany((type) => UserProject, (el) => el.project, { + persistence: false, + onDelete: 'CASCADE', + }) + userProjects!: UserProject[]; } diff --git a/integration/crud-typeorm/projects/user-project.entity.ts b/integration/crud-typeorm/projects/user-project.entity.ts new file mode 100644 index 00000000..cead7b8a --- /dev/null +++ b/integration/crud-typeorm/projects/user-project.entity.ts @@ -0,0 +1,29 @@ +import { Entity, Column, ManyToOne, PrimaryColumn } from 'typeorm'; + +import { User } from '../users/user.entity'; +import { Project } from './project.entity'; + +@Entity('user_projects') +export class UserProject { + @PrimaryColumn() + public projectId!: number; + + @PrimaryColumn() + public userId!: number; + + @Column({ nullable: true }) + public review!: string; + + @ManyToOne((type) => Project, (el) => el.userProjects, { + primary: true, + persistence: false, + onDelete: 'CASCADE', + }) + public project: Project; + + @ManyToOne((type) => User, (el) => el.userProjects, { + primary: true, + persistence: false, + }) + public user: User; +} diff --git a/integration/crud-typeorm/seeds.ts b/integration/crud-typeorm/seeds.ts index 48d80e68..2a02cfba 100644 --- a/integration/crud-typeorm/seeds.ts +++ b/integration/crud-typeorm/seeds.ts @@ -42,7 +42,7 @@ export class Seeds1544303473346 implements MigrationInterface { ('Project20', 'description20', false, 10); `); - // users-profiles + // user-profiles await queryRunner.query(` INSERT INTO public.user_profiles ("name") VALUES ('User1'), @@ -92,6 +92,34 @@ export class Seeds1544303473346 implements MigrationInterface { ('20@email.com', false, 2, 20, NULL, NULL), ('21@email.com', false, 2, NULL, NULL, NULL); `); + + // licenses + await queryRunner.query(` + INSERT INTO public.licenses ("name") VALUES + ('License1'), + ('License2'), + ('License3'), + ('License4'), + ('License5'); + `); + + // user-licenses + await queryRunner.query(` + INSERT INTO public.user_licenses ("userId", "licenseId", "yearsActive") VALUES + (1, 1, 3), + (1, 2, 5), + (1, 4, 7), + (2, 5, 1); + `); + + // user-projects + await queryRunner.query(` + INSERT INTO public.user_projects ("projectId", "userId", "review") VALUES + (1, 1, 'User project 1 1'), + (1, 2, 'User project 1 2'), + (2, 2, 'User project 2 2'), + (3, 3, 'User project 3 3'); + `); } public async down(queryRunner: QueryRunner): Promise {} diff --git a/integration/crud-typeorm/users-licenses/index.ts b/integration/crud-typeorm/users-licenses/index.ts new file mode 100644 index 00000000..2b930782 --- /dev/null +++ b/integration/crud-typeorm/users-licenses/index.ts @@ -0,0 +1,2 @@ +export * from './license.entity'; +export * from './user-license.entity'; diff --git a/integration/crud-typeorm/users-licenses/license.entity.ts b/integration/crud-typeorm/users-licenses/license.entity.ts new file mode 100644 index 00000000..ec213911 --- /dev/null +++ b/integration/crud-typeorm/users-licenses/license.entity.ts @@ -0,0 +1,12 @@ +import { Column, Entity } from 'typeorm'; +import { BaseEntity } from '../base-entity'; +import { IsOptional, IsString, MaxLength } from 'class-validator'; + +@Entity('licenses') +export class License extends BaseEntity { + @IsOptional({ always: true }) + @IsString({ always: true }) + @MaxLength(32, { always: true }) + @Column({ type: 'varchar', length: 32, nullable: true, default: null }) + name: string; +} diff --git a/integration/crud-typeorm/users-licenses/user-license.entity.ts b/integration/crud-typeorm/users-licenses/user-license.entity.ts new file mode 100644 index 00000000..477e87f8 --- /dev/null +++ b/integration/crud-typeorm/users-licenses/user-license.entity.ts @@ -0,0 +1,24 @@ +import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; +import { User } from '../users/user.entity'; +import { Type } from 'class-transformer'; +import { License } from './license.entity'; + +@Entity('user_licenses') +export class UserLicense { + @PrimaryColumn() + userId: number; + + @PrimaryColumn() + licenseId: number; + + @ManyToOne((type) => User) + @Type((t) => User) + user: User; + + @ManyToOne((type) => License) + @Type((t) => License) + license: License; + + @Column() + yearsActive: number; +} diff --git a/integration/crud-typeorm/users/user.entity.ts b/integration/crud-typeorm/users/user.entity.ts index 8cce4bb1..95fb8210 100644 --- a/integration/crud-typeorm/users/user.entity.ts +++ b/integration/crud-typeorm/users/user.entity.ts @@ -1,4 +1,12 @@ -import { Entity, Column, JoinColumn, OneToOne, ManyToOne, ManyToMany } from 'typeorm'; +import { + Entity, + Column, + JoinColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, +} from 'typeorm'; import { IsOptional, IsString, @@ -13,22 +21,23 @@ import { CrudValidationGroups } from '@nestjsx/crud'; import { BaseEntity } from '../base-entity'; import { UserProfile } from '../users-profiles/user-profile.entity'; +import { UserLicense } from '../users-licenses/user-license.entity'; import { Company } from '../companies/company.entity'; import { Project } from '../projects/project.entity'; +import { UserProject } from '../projects/user-project.entity'; const { CREATE, UPDATE } = CrudValidationGroups; -export class Name { +export class Name { @IsString({ always: true }) @Column({ nullable: true }) first: string; - + @IsString({ always: true }) @Column({ nullable: true }) last: string; } - // tslint:disable-next-line:max-classes-per-file @Entity('users') export class User extends BaseEntity { @@ -47,7 +56,7 @@ export class User extends BaseEntity { isActive: boolean; @Type((t) => Name) - @Column(type => Name) + @Column((type) => Name) name: Name; @Column({ nullable: true }) @@ -73,4 +82,15 @@ export class User extends BaseEntity { @ManyToMany((type) => Project, (c) => c.users) projects?: Project[]; + + @OneToMany((type) => UserProject, (el) => el.user, { + persistence: false, + onDelete: 'CASCADE', + }) + userProjects?: UserProject[]; + + @OneToMany((type) => UserLicense, (ul) => ul.user) + @Type((t) => UserLicense) + @JoinColumn() + userLicenses?: UserLicense[]; } diff --git a/packages/crud-typeorm/src/typeorm-crud.service.ts b/packages/crud-typeorm/src/typeorm-crud.service.ts index ebc814f7..515182df 100644 --- a/packages/crud-typeorm/src/typeorm-crud.service.ts +++ b/packages/crud-typeorm/src/typeorm-crud.service.ts @@ -414,10 +414,9 @@ export class TypeOrmCrudService extends CrudService { [curr.propertyName]: { name: curr.propertyName, columns: curr.inverseEntityMetadata.columns.map((col) => col.propertyName), - referencedColumn: (curr.joinColumns.length - ? curr.joinColumns[0] - : curr.inverseRelation.joinColumns[0] - ).referencedColumn.propertyName, + primaryColumns: curr.inverseEntityMetadata.primaryColumns.map( + (col) => col.propertyName, + ), }, }), {}, @@ -521,10 +520,9 @@ export class TypeOrmCrudService extends CrudService { this.entityRelationsHash[cond.field] = { name: curr.propertyName, columns: curr.inverseEntityMetadata.columns.map((col) => col.propertyName), - referencedColumn: (curr.joinColumns.length - ? /* istanbul ignore next */ curr.joinColumns[0] - : curr.inverseRelation.joinColumns[0] - ).referencedColumn.propertyName, + primaryColumns: curr.inverseEntityMetadata.primaryColumns.map( + (col) => col.propertyName, + ), nestedRelation: curr.nestedRelation, }; } @@ -548,7 +546,7 @@ export class TypeOrmCrudService extends CrudService { : cond.select.filter((col) => allowed.some((a) => a === col)); const select = [ - relation.referencedColumn, + ...relation.primaryColumns, ...(options.persist && options.persist.length ? options.persist : []), ...columns, ].map((col) => `${alias}.${col}`); diff --git a/packages/crud-typeorm/test/b.query-params.spec.ts b/packages/crud-typeorm/test/b.query-params.spec.ts index c277b9a8..785b65cd 100644 --- a/packages/crud-typeorm/test/b.query-params.spec.ts +++ b/packages/crud-typeorm/test/b.query-params.spec.ts @@ -52,6 +52,8 @@ describe('#crud-typeorm', () => { persist: ['id'], exclude: ['updatedAt', 'createdAt'], }, + users: {}, + userProjects: {}, }, sort: [{ field: 'id', order: 'ASC' }], limit: 100, @@ -98,6 +100,7 @@ describe('#crud-typeorm', () => { join: { company: {}, 'company.projects': {}, + userLicenses: {}, }, }, }) @@ -437,6 +440,39 @@ describe('#crud-typeorm', () => { done(); }); }); + it('should return joined entity with ManyToMany pivot table', (done) => { + const query = qb + .setJoin({ field: 'users' }) + .setJoin({ field: 'userProjects' }) + .query(); + return request(server) + .get('/projects/1') + .query(query) + .end((_, res) => { + expect(res.status).toBe(200); + expect(res.body.users).toBeDefined(); + expect(res.body.users.length).toBe(2); + expect(res.body.users[0].id).toBe(1); + expect(res.body.userProjects).toBeDefined(); + expect(res.body.userProjects.length).toBe(2); + expect(res.body.userProjects[0].review).toBe('User project 1 1'); + done(); + }); + }); + }); + + describe('#query composite key join', () => { + it('should return joined relation', (done) => { + const query = qb.setJoin({ field: 'userLicenses' }).query(); + return request(server) + .get('/users/1') + .query(query) + .end((_, res) => { + expect(res.status).toBe(200); + expect(res.body.userLicenses).toBeDefined(); + done(); + }); + }); }); describe('#sort', () => { diff --git a/packages/crud-typeorm/test/c.basic-crud.spec.ts b/packages/crud-typeorm/test/c.basic-crud.spec.ts index b08b47e0..22fed2b2 100644 --- a/packages/crud-typeorm/test/c.basic-crud.spec.ts +++ b/packages/crud-typeorm/test/c.basic-crud.spec.ts @@ -280,7 +280,7 @@ describe('#crud-typeorm', () => { }); }); it('should return saved entity with param', (done) => { - const dto: User = { + const dto: any = { email: 'test@test.com', isActive: true, name: {