diff --git a/examples/typeorm-soft-delete/e2e/fixtures.ts b/examples/typeorm-soft-delete/e2e/fixtures.ts index cb35da1e3..2f759c432 100644 --- a/examples/typeorm-soft-delete/e2e/fixtures.ts +++ b/examples/typeorm-soft-delete/e2e/fixtures.ts @@ -2,7 +2,9 @@ import { Connection } from 'typeorm' import { executeTruncate } from '../../../examples/helpers' import { SubTaskEntity } from '../src/sub-task/sub-task.entity' +import { TagEntity } from '../src/tag/tag.entity' import { TodoItemEntity } from '../src/todo-item/todo-item.entity' +import { TodoToTagEntity } from '../src/todo-to-tag/todo-to-tag.entity' const tables = ['todo_item'] export const truncate = async (connection: Connection): Promise => executeTruncate(connection, tables) @@ -13,7 +15,10 @@ export const refresh = async (connection: Connection): Promise => { const todoRepo = connection.getRepository(TodoItemEntity) const subTaskRepo = connection.getRepository(SubTaskEntity) - const savedTodos = await todoRepo.save( + const tagRepo = connection.getRepository(TagEntity) + const todoToTagRepo = connection.getRepository(TodoToTagEntity) + + const savedTodoItems = await todoRepo.save( todoRepo.create([ { title: 'Create Nest App', completed: true }, { title: 'Create Entity', completed: false }, @@ -24,7 +29,7 @@ export const refresh = async (connection: Connection): Promise => { ) await subTaskRepo.save( - savedTodos.reduce( + savedTodoItems.reduce( (subTasks, todo) => [ ...subTasks, @@ -35,4 +40,24 @@ export const refresh = async (connection: Connection): Promise => { [] as Partial[] ) ) + + const tags = await tagRepo.save(tagRepo.create([{ name: 'To Review' }, { name: 'Reviewed' }])) + + await todoToTagRepo.save( + todoToTagRepo.create([ + { todoID: savedTodoItems[0].id, tagID: tags[0].id }, + + { todoID: savedTodoItems[1].id, tagID: tags[0].id }, + + { todoID: savedTodoItems[2].id, tagID: tags[0].id }, + + // Mark one of the links as deleted. + { todoID: savedTodoItems[3].id, tagID: tags[0].id, deleted: new Date() }, + { todoID: savedTodoItems[3].id, tagID: tags[1].id }, + + // Mark one of the links as deleted. + { todoID: savedTodoItems[4].id, tagID: tags[0].id, deleted: new Date() }, + { todoID: savedTodoItems[4].id, tagID: tags[1].id } + ]) + ) } diff --git a/examples/typeorm-soft-delete/e2e/graphql-fragments.ts b/examples/typeorm-soft-delete/e2e/graphql-fragments.ts index 975226ccf..d334c73fc 100644 --- a/examples/typeorm-soft-delete/e2e/graphql-fragments.ts +++ b/examples/typeorm-soft-delete/e2e/graphql-fragments.ts @@ -5,6 +5,17 @@ export const todoItemFields = ` description ` +export const todoItemWithTagsFields = ` + id + title + toTags { + tag { + id + name + } + } +` + export const pageInfoField = ` pageInfo{ hasNextPage diff --git a/examples/typeorm-soft-delete/e2e/todo-item.resolver.spec.ts b/examples/typeorm-soft-delete/e2e/todo-item.resolver.spec.ts index 310a2186e..25baef683 100644 --- a/examples/typeorm-soft-delete/e2e/todo-item.resolver.spec.ts +++ b/examples/typeorm-soft-delete/e2e/todo-item.resolver.spec.ts @@ -9,7 +9,7 @@ import { AppModule } from '../src/app.module' import { SubTaskEntity } from '../src/sub-task/sub-task.entity' import { TodoItemDTO } from '../src/todo-item/dto/todo-item.dto' import { refresh } from './fixtures' -import { edgeNodes, pageInfoField, todoItemFields } from './graphql-fragments' +import { edgeNodes, pageInfoField, todoItemFields, todoItemWithTagsFields } from './graphql-fragments' describe('SoftDelete - TodoItemResolver (e2e)', () => { let app: INestApplication @@ -338,6 +338,43 @@ describe('SoftDelete - TodoItemResolver (e2e)', () => { }) }) + it(`should return the todos with their tags`, async () => { + await request(app.getHttpServer()) + .post('/graphql') + .send({ + operationName: null, + variables: {}, + query: `{ + todoItems { + ${pageInfoField} + ${edgeNodes(todoItemWithTagsFields)} + } + }` + }) + .expect(200) + .then(({ body }) => { + const { edges, pageInfo }: CursorConnectionType = body.data.todoItems + expect(pageInfo).toEqual({ + endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=', + hasNextPage: false, + hasPreviousPage: false, + startCursor: 'YXJyYXljb25uZWN0aW9uOjA=' + }) + expect(edges).toHaveLength(5) + expect(edges.map((e) => e.node)).toEqual([ + { id: '1', title: 'Create Nest App', toTags: [{ tag: { id: '1', name: 'To Review' } }] }, + { id: '2', title: 'Create Entity', toTags: [{ tag: { id: '1', name: 'To Review' } }] }, + { id: '3', title: 'Create Entity Service', toTags: [{ tag: { id: '1', name: 'To Review' } }] }, + { + id: '4', + title: 'Add Todo Item Resolver', + toTags: [{ tag: { id: '2', name: 'Reviewed' } }] + }, + { id: '5', title: 'How to create item With Sub Tasks', toTags: [{ tag: { id: '2', name: 'Reviewed' } }] } + ]) + }) + }) + describe('paging', () => { it(`should allow paging with the 'first' field`, () => request(app.getHttpServer()) diff --git a/examples/typeorm-soft-delete/schema.gql b/examples/typeorm-soft-delete/schema.gql index b28f6901a..8b46dc78c 100644 --- a/examples/typeorm-soft-delete/schema.gql +++ b/examples/typeorm-soft-delete/schema.gql @@ -2,6 +2,17 @@ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ +type Tag { + id: ID! + name: String! +} + +type TodoItemToTag { + todoID: ID! + tagID: ID! + tag: Tag +} + type TodoItem { id: ID! title: String! @@ -17,6 +28,13 @@ type TodoItem { """Specify to sort results.""" sorting: [SubTaskSort!]! = [] ): [SubTask!]! + toTags( + """Specify to filter the records returned.""" + filter: TodoItemToTagFilter! = {} + + """Specify to sort results.""" + sorting: [TodoItemToTagSort!]! = [] + ): [TodoItemToTag!]! } """ @@ -160,6 +178,24 @@ enum SortNulls { NULLS_LAST } +input TodoItemToTagFilter { + and: [TodoItemToTagFilter!] + or: [TodoItemToTagFilter!] + todoID: IDFilterComparison + tagID: IDFilterComparison +} + +input TodoItemToTagSort { + field: TodoItemToTagSortFields! + direction: SortDirection! + nulls: SortNulls +} + +enum TodoItemToTagSortFields { + todoID + tagID +} + type SubTask { id: ID! title: String! diff --git a/examples/typeorm-soft-delete/src/tag/dto/tag.dto.ts b/examples/typeorm-soft-delete/src/tag/dto/tag.dto.ts new file mode 100644 index 000000000..abfc034ed --- /dev/null +++ b/examples/typeorm-soft-delete/src/tag/dto/tag.dto.ts @@ -0,0 +1,11 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql' +import { FilterableField } from '@ptc-org/nestjs-query-graphql' + +@ObjectType('Tag') +export class TagDTO { + @FilterableField(() => ID) + id!: number + + @Field() + name!: string +} diff --git a/examples/typeorm-soft-delete/src/tag/tag.entity.ts b/examples/typeorm-soft-delete/src/tag/tag.entity.ts new file mode 100644 index 000000000..44f250329 --- /dev/null +++ b/examples/typeorm-soft-delete/src/tag/tag.entity.ts @@ -0,0 +1,24 @@ +import { Column, CreateDateColumn, DeleteDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm' + +import { TodoToTagEntity } from '../todo-to-tag/todo-to-tag.entity' + +@Entity({ name: 'tag' }) +export class TagEntity { + @PrimaryGeneratedColumn() + id!: number + + @Column() + name!: string + + @OneToMany(() => TodoToTagEntity, (todoToTag) => todoToTag.tag) + toTodos!: TodoToTagEntity[] + + @CreateDateColumn() + created!: Date + + @UpdateDateColumn() + updated!: Date + + @DeleteDateColumn() + deleted?: Date +} diff --git a/examples/typeorm-soft-delete/src/tag/tag.service.ts b/examples/typeorm-soft-delete/src/tag/tag.service.ts new file mode 100644 index 000000000..53cddc73a --- /dev/null +++ b/examples/typeorm-soft-delete/src/tag/tag.service.ts @@ -0,0 +1,13 @@ +import { InjectRepository } from '@nestjs/typeorm' +import { QueryService } from '@ptc-org/nestjs-query-core' +import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm' +import { Repository } from 'typeorm' + +import { TagEntity } from './tag.entity' + +@QueryService(TagEntity) +export class TagService extends TypeOrmQueryService { + constructor(@InjectRepository(TagEntity) repo: Repository) { + super(repo, { useSoftDelete: true }) + } +} diff --git a/examples/typeorm-soft-delete/src/todo-item/dto/todo-item.dto.ts b/examples/typeorm-soft-delete/src/todo-item/dto/todo-item.dto.ts index 7a152de71..bf43c2f6b 100644 --- a/examples/typeorm-soft-delete/src/todo-item/dto/todo-item.dto.ts +++ b/examples/typeorm-soft-delete/src/todo-item/dto/todo-item.dto.ts @@ -1,14 +1,17 @@ import { GraphQLISODateTime, ID, ObjectType } from '@nestjs/graphql' -import { FilterableField, FilterableUnPagedRelation } from '@ptc-org/nestjs-query-graphql' +import { FilterableField, FilterableUnPagedRelation, UnPagedRelation } from '@ptc-org/nestjs-query-graphql' import { SubTaskDTO } from '../../sub-task/dto/sub-task.dto' import { SubTaskEntity } from '../../sub-task/sub-task.entity' +import { TodoToTagDTO } from '../../todo-to-tag/dto/todo-to-tag.dto' +import { TodoToTagEntity } from '../../todo-to-tag/todo-to-tag.entity' @ObjectType('TodoItem') @FilterableUnPagedRelation('subTasks', () => SubTaskDTO, { update: { enabled: true }, withDeleted: true }) +@UnPagedRelation('toTags', () => TodoToTagDTO) export class TodoItemDTO { @FilterableField(() => ID) id!: number @@ -22,6 +25,8 @@ export class TodoItemDTO { @FilterableField() completed!: boolean + toTags!: TodoToTagEntity[] + @FilterableField() subTasksCount!: number diff --git a/examples/typeorm-soft-delete/src/todo-item/todo-item.entity.ts b/examples/typeorm-soft-delete/src/todo-item/todo-item.entity.ts index 2c531bca2..2a195f4e9 100644 --- a/examples/typeorm-soft-delete/src/todo-item/todo-item.entity.ts +++ b/examples/typeorm-soft-delete/src/todo-item/todo-item.entity.ts @@ -10,6 +10,7 @@ import { } from 'typeorm' import { SubTaskEntity } from '../sub-task/sub-task.entity' +import { TodoToTagEntity } from '../todo-to-tag/todo-to-tag.entity' @Entity({ name: 'todo_item' }) export class TodoItemEntity { @@ -28,6 +29,9 @@ export class TodoItemEntity { @OneToMany(() => SubTaskEntity, (subTask) => subTask.todoItem) subTasks!: SubTaskEntity[] + @OneToMany(() => TodoToTagEntity, (todoToTag) => todoToTag.todoItem) + toTags!: TodoToTagEntity[] + @VirtualColumn({ query: (alias) => `SELECT COUNT(*) FROM sub_task diff --git a/examples/typeorm-soft-delete/src/todo-item/todo-item.module.ts b/examples/typeorm-soft-delete/src/todo-item/todo-item.module.ts index f41ed2c36..d6484c32a 100644 --- a/examples/typeorm-soft-delete/src/todo-item/todo-item.module.ts +++ b/examples/typeorm-soft-delete/src/todo-item/todo-item.module.ts @@ -1,7 +1,13 @@ import { Module } from '@nestjs/common' -import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql' +import { NestjsQueryGraphQLModule, PagingStrategies } from '@ptc-org/nestjs-query-graphql' import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm' +import { TagDTO } from '../tag/dto/tag.dto' +import { TagEntity } from '../tag/tag.entity' +import { TagService } from '../tag/tag.service' +import { TodoToTagDTO } from '../todo-to-tag/dto/todo-to-tag.dto' +import { TodoToTagEntity } from '../todo-to-tag/todo-to-tag.entity' +import { TodoToTagService } from '../todo-to-tag/todo-to-tag.service' import { TodoItemDTO } from './dto/todo-item.dto' import { TodoItemInputDTO } from './dto/todo-item-input.dto' import { TodoItemUpdateDTO } from './dto/todo-item-update.dto' @@ -13,14 +19,34 @@ import { TodoItemService } from './todo-item.service' providers: [TodoItemResolver], imports: [ NestjsQueryGraphQLModule.forFeature({ - imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity])], - services: [TodoItemService], + imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity, TodoToTagEntity, TagEntity])], + services: [TodoItemService, TodoToTagService, TagService], resolvers: [ { DTOClass: TodoItemDTO, ServiceClass: TodoItemService, CreateDTOClass: TodoItemInputDTO, UpdateDTOClass: TodoItemUpdateDTO + }, + { + DTOClass: TodoToTagDTO, + EntityClass: TodoToTagEntity, + ServiceClass: TodoToTagService, + pagingStrategy: PagingStrategies.NONE, + create: { disabled: true }, + update: { disabled: true }, + delete: { disabled: true }, + read: { disabled: true } + }, + { + DTOClass: TagDTO, + EntityClass: TagEntity, + ServiceClass: TagService, + pagingStrategy: PagingStrategies.NONE, + create: { disabled: true }, + update: { disabled: true }, + delete: { disabled: true }, + read: { disabled: true } } ] }) diff --git a/examples/typeorm-soft-delete/src/todo-to-tag/dto/todo-to-tag.dto.ts b/examples/typeorm-soft-delete/src/todo-to-tag/dto/todo-to-tag.dto.ts new file mode 100644 index 000000000..6570a7771 --- /dev/null +++ b/examples/typeorm-soft-delete/src/todo-to-tag/dto/todo-to-tag.dto.ts @@ -0,0 +1,19 @@ +import { ID, ObjectType } from '@nestjs/graphql' +import { FilterableField, Relation } from '@ptc-org/nestjs-query-graphql' + +import { TagDTO } from '../../tag/dto/tag.dto' +import { TodoItemEntity } from '../../todo-item/todo-item.entity' + +@ObjectType('TodoItemToTag') +@Relation('tag', () => TagDTO, { nullable: true }) +export class TodoToTagDTO { + @FilterableField(() => ID) + todoID: number + + todoItem: TodoItemEntity + + @FilterableField(() => ID) + tagID: number + + tag!: TagDTO +} diff --git a/examples/typeorm-soft-delete/src/todo-to-tag/todo-to-tag.entity.ts b/examples/typeorm-soft-delete/src/todo-to-tag/todo-to-tag.entity.ts new file mode 100644 index 000000000..cf4a00a92 --- /dev/null +++ b/examples/typeorm-soft-delete/src/todo-to-tag/todo-to-tag.entity.ts @@ -0,0 +1,24 @@ +import { DeleteDateColumn, Entity, JoinColumn, ManyToOne, ObjectType, PrimaryColumn } from 'typeorm' + +import { TagEntity } from '../tag/tag.entity' +import { TodoItemEntity } from '../todo-item/todo-item.entity' + +@Entity({ name: 'todo_to_tag' }) +export class TodoToTagEntity { + @PrimaryColumn({ name: 'todo_id' }) + todoID!: number + + @ManyToOne((): ObjectType => TodoItemEntity, (t) => t.toTags) + @JoinColumn({ name: 'todo_id' }) + todoItem: TodoItemEntity + + @PrimaryColumn({ name: 'tag_id' }) + tagID!: number + + @ManyToOne((): ObjectType => TagEntity, (t) => t.toTodos) + @JoinColumn({ name: 'tag_id' }) + tag: TagEntity + + @DeleteDateColumn() + deleted?: Date +} diff --git a/examples/typeorm-soft-delete/src/todo-to-tag/todo-to-tag.service.ts b/examples/typeorm-soft-delete/src/todo-to-tag/todo-to-tag.service.ts new file mode 100644 index 000000000..29be830a7 --- /dev/null +++ b/examples/typeorm-soft-delete/src/todo-to-tag/todo-to-tag.service.ts @@ -0,0 +1,13 @@ +import { InjectRepository } from '@nestjs/typeorm' +import { QueryService } from '@ptc-org/nestjs-query-core' +import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm' +import { Repository } from 'typeorm' + +import { TodoToTagEntity } from './todo-to-tag.entity' + +@QueryService(TodoToTagEntity) +export class TodoToTagService extends TypeOrmQueryService { + constructor(@InjectRepository(TodoToTagEntity) repo: Repository) { + super(repo, { useSoftDelete: true }) + } +}