MichalLytek / typegraphql-prisma

Prisma generator to emit TypeGraphQL types and CRUD resolvers from your Prisma schema
https://prisma.typegraphql.com
MIT License
891 stars 113 forks source link

FindMany nested object queries returning NULL #367

Closed spencerbull closed 1 year ago

spencerbull commented 1 year ago

Describe the Bug FindMany nested object queries are returning NULL for the Many-to-One query use case. For the example below, I have a database in a star schema with fact and dimension tables. The FindMany queries return valid children when making queries in a One-to-Many direction, but return NULL in a Many-to-one Direction.

To Reproduce Using typegraphql generator with basic configuration and simplified data model

generator typegraphql {
    provider = "typegraphql-prisma"
}

model DimJobRole {
    JobRoleId                 BigInt                         @default(autoincrement())
    JobFunctionName    String                         @db.VarChar(128)
    JobRoleEvents          FactJobRoleEvent[]
}

model FactJobRoleEvent {
    JobRoleEventId    BigInt                              @id(map: "PK__1832492348")
    JobRoleId             BigInt                              
    DimJobRole          DimJobRole                    @relation(fields: [JobRoleId], references: [JobRoleId], onUpdate: NoAction, map: "FK_FactJobRoleEvents_DimJobRole"
}

Example query FindMany queries are returning null.

query FactJobRoleEvents($take: Int) {
  factJobRoleEvent(take: $take) {
    jobRoleEventId
    jobRoleEventName
    DimJobRole {
      JobFunctionName
    }
  }
}

This query results in an error since DimJobRole cannot be null. I did confirm by making the following generated relations resolver nullable set to true. If the take parameter is set to 1, then the query resolves, if the query is set to >1, then the query returns null for all children.

Attached below in the logs section are the Prisma queries that are being run with verbose debug logs.

Expected Behavior When making a GraphQL findMany query in the direction of Many-to-One, the children should return valid objects corresponding the to the table.

Logs For take set to 1

prisma:client:libraryEngine  sending request, this.libraryStarted: true +45ms
prisma:query SELECT ....(full table).... WHERE ([dbo].[FactJobRoleEvent].[JobRoleEventId] = @P1 AND 1=1) ORDER BY 1 OFFSET @P2 ROWS FETCH NEXT @P3 ROWS ONLY
prisma:query SELECT [dbo].[DimJobRole].[JobRoleId] FROM [dbo].[DimJobRole] WHERE [dbo].[DimJobRole].[JobRoleId] IN (@P1)

For take set to 2

prisma:query SELECT ....(full table) WHERE 1=1 ORDER BY [dbo].[FactJobRoleEvnet].[JobRoleEventId] ASC OFFSET @P1 ROWS FETCH NEXT @P2 ROWS ONLY
prisma:query SELECT ....(query fields) .... FROM [dbo].[FactJobRoleEvnet] WHERE [dbo].[FactJobRoleEvnet].[JobRoleEventId] IN (@P1,@P2)
prisma:query SELECT ....(full table).... WHERE [dbo].[DimJobRole].[JobRoleId] IN (@P1,@P2)

Environment:

Aditional Context

Possibly similar to #248

spencerbull commented 1 year ago

I had a quick update I wanted to provide. In my relationResolvers. I found that replacing the following findUnique to findUniqueOrThrow is now returning values. For a temporary fix, I'm updating the generated code.

Example of the change...

Generated Relation Resolvers

@TypeGraphQL.FieldResolver(_type => DimJobRole, {
    nullable: false
  })
  async DimJobRole(@TypeGraphQL.Root() factJobRoleEvent: FactJobRoleEvent, @TypeGraphQL.Ctx() ctx: any, @TypeGraphQL.Info() info: GraphQLResolveInfo): Promise<DimJobRole> {
    const { _count } = transformInfoIntoPrismaArgs(info);
    return getPrismaFromContext(ctx).factJobRoleEvent.findUnique({
      where: {
        FactJobEventId: factJobRoleEvent.jobRoleEventId,
      },
    }).DimCalendar({
      ...(_count && transformCountFieldIntoSelectRelationsCount(_count)),
    });
  }

Updated Relation Resolvers

@TypeGraphQL.FieldResolver(_type => DimJobRole, {
    nullable: false
  })
  async DimJobRole(@TypeGraphQL.Root() factJobRoleEvent: FactJobRoleEvent, @TypeGraphQL.Ctx() ctx: any, @TypeGraphQL.Info() info: GraphQLResolveInfo): Promise<DimJobRole> {
    const { _count } = transformInfoIntoPrismaArgs(info);
    return getPrismaFromContext(ctx).factJobRoleEvent.findUniqueOrThrow({
      where: {
        FactJobEventId: factJobRoleEvent.jobRoleEventId,
      },
    }).DimCalendar({
      ...(_count && transformCountFieldIntoSelectRelationsCount(_count)),
    });
  }

I would recommend not marking this issue as fixed, but I did want to highlight a path forward here with an update.

MichalLytek commented 1 year ago

I am able to reproduce the issue ๐Ÿ‘

However, after digging a bit I think you've discovered an issue in Prisma Client built-in data-loader ๐Ÿ˜„

Let's reproduce the issue:

Schema:

model DimJobRole {
  JobRoleId       BigInt             @id @default(autoincrement())
  JobFunctionName String             @db.VarChar(128)
  JobRoleEvents   FactJobRoleEvent[]
}

model FactJobRoleEvent {
  JobRoleEventId BigInt     @id(map: "PK__1832492348")
  JobRoleId      BigInt
  DimJobRole     DimJobRole @relation(fields: [JobRoleId], references: [JobRoleId], onUpdate: NoAction, map: "FK_FactJobRoleEvents_DimJobRole")
}

Seed:

const [job1, job2, job3] = await Promise.all([
    prisma.dimJobRole.create({
      data: { JobFunctionName: "JobFunctionName 1" },
    }),
    prisma.dimJobRole.create({
      data: { JobFunctionName: "JobFunctionName 2" },
    }),
    prisma.dimJobRole.create({
      data: { JobFunctionName: "JobFunctionName 3" },
    }),
  ]);

  await Promise.all([
    prisma.factJobRoleEvent.create({
      data: {
        JobRoleEventId: 1,
        DimJobRole: {
          connect: { JobRoleId: job1.JobRoleId },
        },
      },
    }),
    prisma.factJobRoleEvent.create({
      data: {
        JobRoleEventId: 2,
        DimJobRole: {
          connect: { JobRoleId: job1.JobRoleId },
        },
      },
    }),
    prisma.factJobRoleEvent.create({
      data: {
        JobRoleEventId: 3,
        DimJobRole: {
          connect: { JobRoleId: job2.JobRoleId },
        },
      },
    }),
  ]);

Script 1 - await one by one:

const factJobRoleEvents = await prisma.factJobRoleEvent.findMany({
    take: 3,
  });
  console.log(factJobRoleEvents);
  for (const factJobRoleEvent of factJobRoleEvents) {
    const dimJobRole = await prisma.factJobRoleEvent
      .findUnique({
        where: {
          JobRoleEventId: factJobRoleEvent.JobRoleEventId,
        },
      })
      .DimJobRole({});
    console.log({dimJobRole});
  }

Logs:

[
  { JobRoleEventId: 1n, JobRoleId: 23n },
  { JobRoleEventId: 2n, JobRoleId: 23n },
  { JobRoleEventId: 3n, JobRoleId: 22n }
]
{
  dimJobRole: { JobRoleId: 23n, JobFunctionName: 'JobFunctionName 1' }
}
{
  dimJobRole: { JobRoleId: 23n, JobFunctionName: 'JobFunctionName 1' }
}
{
  dimJobRole: { JobRoleId: 22n, JobFunctionName: 'JobFunctionName 2' }
}

Script 2 - await Promise.all (like paralel field resolvers execution):

const factJobRoleEvents = await prisma.factJobRoleEvent.findMany({
    take: 3,
  });
  console.log(factJobRoleEvents);

  await Promise.all(
    factJobRoleEvents.map(async factJobRoleEvent => {
      const dimJobRole = await prisma.factJobRoleEvent
        .findUnique({
          where: {
            JobRoleEventId: factJobRoleEvent.JobRoleEventId,
          },
        })
        .DimJobRole({});
      console.log({ dimJobRole });
    }),
  );

Logs:

[
  { JobRoleEventId: 1n, JobRoleId: 19n },
  { JobRoleEventId: 2n, JobRoleId: 19n },
  { JobRoleEventId: 3n, JobRoleId: 21n }
]
{ dimJobRole: null }
{ dimJobRole: null }
{ dimJobRole: null }

Script 3 - promise all + OrThrow:

const factJobRoleEvents = await prisma.factJobRoleEvent.findMany({
    take: 3,
  });
  console.log(factJobRoleEvents);

await Promise.all(
    factJobRoleEvents.map(async factJobRoleEvent => {
      const dimJobRole = await prisma.factJobRoleEvent
        .findUniqueOrThrow({
          where: {
            JobRoleEventId: factJobRoleEvent.JobRoleEventId,
          },
        })
        .DimJobRole({});
      console.log({ dimJobRole });
    }),
  );

Logs:

[
  { JobRoleEventId: 1n, JobRoleId: 25n },
  { JobRoleEventId: 2n, JobRoleId: 25n },
  { JobRoleEventId: 3n, JobRoleId: 26n }
]
{
  dimJobRole: { JobRoleId: 25n, JobFunctionName: 'JobFunctionName 1' }
}
{
  dimJobRole: { JobRoleId: 25n, JobFunctionName: 'JobFunctionName 1' }
}
{
  dimJobRole: { JobRoleId: 26n, JobFunctionName: 'JobFunctionName 2' }
}
MichalLytek commented 1 year ago

Reported in Prisma: https://github.com/prisma/prisma/issues/18326

But I will fix this issue by just using findUniqueOrThrow - it's a proper way anyway, as the record has to exist as it was returned from a parent db query ๐Ÿ‘€

MichalLytek commented 1 year ago

Fixed in 3ea20f6 - @spencerbull let me know if the fix does not work for you ๐Ÿ˜‰

spencerbull commented 1 year ago

This looks great thanks!