TriPSs / nestjs-query

Easy CRUD for GraphQL.
https://tripss.github.io/nestjs-query/
MIT License
157 stars 43 forks source link

Composite primary keys generating an incorrect schema #267

Open ValentinVignal opened 5 months ago

ValentinVignal commented 5 months ago

Describe the bug

I'm using an entity with composite primary keys. Those primary keys are not used in the generated schema for the read-one, update-one, and delete-one queries/mutations.

Have you read the Contributing Guidelines?

Yes.

To Reproduce

Follow the installation for the example: https://tripss.github.io/nestjs-query/docs/introduction/example

Use this entity:

import { ID, ObjectType } from '@nestjs/graphql';
import { FilterableField, IDField } from '@ptc-org/nestjs-query-graphql';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
@ObjectType('TodoItem')
export class TodoItemEntity {
  @IDField(() => ID)
  @PrimaryGeneratedColumn()
  firstId!: string;

  @IDField(() => ID)
  @PrimaryGeneratedColumn()
  secondId!: string;

  @FilterableField()
  @Column()
  title!: string;
}

And this module:

import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { Module } from '@nestjs/common';
import { TodoItemEntity } from './todo-item.entity';

@Module({
  imports: [
    NestjsQueryGraphQLModule.forFeature({
      // import the NestjsQueryTypeOrmModule to register the entity with typeorm
      // and provide a QueryService
      imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity])],
      resolvers: [
        {
          EntityClass: TodoItemEntity,
          DTOClass: TodoItemEntity,
        },
      ],
    }),
  ],
})
export class TodoItemModule {}

Expected behavior

The schema generated is using id: ID! for the read-one, update-one, and delete-one queries/mutations.

Generated schema ```graphql type TodoItem { firstId: ID! secondId: ID! title: String! } type DeleteManyResponse { # The number of records deleted. deletedCount: Int! } type TodoItemDeleteResponse { firstId: ID secondId: ID title: String } type UpdateManyResponse { # The number of records updated. updatedCount: Int! } type TodoItemEdge { # The node containing the TodoItem node: TodoItem! # Cursor for this node. cursor: ConnectionCursor! } # Cursor for paging through collections scalar ConnectionCursor type PageInfo { # true if paging forward and there are more records. hasNextPage: Boolean # true if paging backwards and there are more records. hasPreviousPage: Boolean # The cursor of the first returned record. startCursor: ConnectionCursor # The cursor of the last returned record. endCursor: ConnectionCursor } type TodoItemConnection { # Paging information pageInfo: PageInfo! # Array of edges. edges: [TodoItemEdge!]! } type Query { todoItem( # The id of the record to find. id: ID! ): TodoItem! todoItems( # Limit or page results. paging: CursorPaging! = { first: 10 } # Specify to filter the records returned. filter: TodoItemFilter! = {} # Specify to sort results. sorting: [TodoItemSort!]! = [] ): TodoItemConnection! } input CursorPaging { # Paginate before opaque cursor before: ConnectionCursor # Paginate after opaque cursor after: ConnectionCursor # Paginate first first: Int # Paginate last last: Int } input TodoItemFilter { and: [TodoItemFilter!] or: [TodoItemFilter!] firstId: IDFilterComparison secondId: IDFilterComparison title: StringFieldComparison } input IDFilterComparison { is: Boolean isNot: Boolean eq: ID neq: ID gt: ID gte: ID lt: ID lte: ID like: ID notLike: ID iLike: ID notILike: ID in: [ID!] notIn: [ID!] } input StringFieldComparison { is: Boolean isNot: Boolean eq: String neq: String gt: String gte: String lt: String lte: String like: String notLike: String iLike: String notILike: String in: [String!] notIn: [String!] } input TodoItemSort { field: TodoItemSortFields! direction: SortDirection! nulls: SortNulls } enum TodoItemSortFields { firstId secondId title } # Sort Directions enum SortDirection { ASC DESC } # Sort Nulls Options enum SortNulls { NULLS_FIRST NULLS_LAST } type Mutation { createOneTodoItem(input: CreateOneTodoItemInput!): TodoItem! createManyTodoItems(input: CreateManyTodoItemsInput!): [TodoItem!]! updateOneTodoItem(input: UpdateOneTodoItemInput!): TodoItem! updateManyTodoItems(input: UpdateManyTodoItemsInput!): UpdateManyResponse! deleteOneTodoItem(input: DeleteOneTodoItemInput!): TodoItemDeleteResponse! deleteManyTodoItems(input: DeleteManyTodoItemsInput!): DeleteManyResponse! } input CreateOneTodoItemInput { # The record to create todoItem: CreateTodoItem! } input CreateTodoItem { firstId: ID! secondId: ID! title: String! } input CreateManyTodoItemsInput { # Array of records to create todoItems: [CreateTodoItem!]! } input UpdateOneTodoItemInput { # The id of the record to update id: ID! # The update to apply. update: UpdateTodoItem! } input UpdateTodoItem { firstId: ID secondId: ID title: String } input UpdateManyTodoItemsInput { # Filter used to find fields to update filter: TodoItemUpdateFilter! # The update to apply to all records found using the filter update: UpdateTodoItem! } input TodoItemUpdateFilter { and: [TodoItemUpdateFilter!] or: [TodoItemUpdateFilter!] firstId: IDFilterComparison secondId: IDFilterComparison title: StringFieldComparison } input DeleteOneTodoItemInput { # The id of the record to delete. id: ID! } input DeleteManyTodoItemsInput { # Filter to find records to delete filter: TodoItemDeleteFilter! } input TodoItemDeleteFilter { and: [TodoItemDeleteFilter!] or: [TodoItemDeleteFilter!] firstId: IDFilterComparison secondId: IDFilterComparison title: StringFieldComparison } ```

I would expect something like:

type Query {
  todoItem(
-    # The id of the record to find.
-    id: ID!
+    firstId: ID!
+    secondId: ID!
  ): TodoItem!
}

input UpdateOneTodoItemInput {
-  # The id of the record to update
-  id: ID!
+  firstId: ID!
+  secondId: ID!

  # The update to apply.
  update: UpdateTodoItem!
}

input DeleteOneTodoItemInput {
-  # The id of the record to delete.
-  id: ID!
+  firstId: ID!
+  secondId: ID!
}

Desktop (please complete the following information):

TriPSs commented 4 months ago

Interesting case, if I look at the code everywhere where we use IDField it only expects one field, to change this will be a challenge as we would then go from one known field (id) to 1 or more unknown fields.

Less then ideal solution: mark the second primary field as a required filter field?