typegraphql-prisma icon indicating copy to clipboard operation
typegraphql-prisma copied to clipboard

Error setting json value to null

Open mdesousa opened this issue 2 years ago • 8 comments

You cannot set a nullable json value to null

To Reproduce define a json column in the prisma schema like this:

model MyTable {
  id String @id @db.Uuid
  myColumn Json?
}

then build graphql schema and execute the following mutation

mutation {
  updateMyTable(data: { myColumn: null},
  where: {id: "3e48e900-20f3-4cce-899e-f6323525645d"}) {
    id
    myColumn
  }
}

Expected Behavior The value should be updated to null

Logs

[GraphQLError [Object]: 
Invalid `helpers_1.getPrismaFromContext)(ctx).myTable.update()` invocation in
/node_modules/@generated/type-graphql/resolvers/crud/MyTable/MyTableCrudResolver.js:69:68

   66 }
   67 async updateMyTable(ctx, info, args) {
   68     const { _count } = (0, helpers_1.transformFields)((0, graphql_fields_1.default)(info));
→  69     return (0, helpers_1.getPrismaFromContext)(ctx).myTable.update({
            where: {
              id: '3e48e900-20f3-4cce-899e-f6323525645d'
            },
            data: {
              myColumn: null
                                 ~~~~
            }
          })

Argument myColumn for data.myColumn must not be null. Please use undefined instead.

] {
  locations: [ { line: 2, column: 3 } ],
  path: [ 'updateMyTable' ],
  extensions: {
    requestId: '97562b3c-d5ad-4a1d-b944-3fb9f9729e82',
    code: 'INTERNAL_SERVER_ERROR'
  }
}

Package versions: [email protected] [email protected] [email protected]

mdesousa avatar Dec 23 '21 22:12 mdesousa

How do you set it to null in raw prisma client?

MichalLytek avatar Dec 24 '21 06:12 MichalLytek

Looks like there is a special enum value to do it: Prisma.DbNull From Working with json fields

import { Prisma } from '@prisma/client'

prisma.log.create({
  data: {
    meta: Prisma.DbNull,
  },
})

mdesousa avatar Dec 24 '21 13:12 mdesousa

export const DbNull: 'DbNull'

So can you execute such mutation?

mutation {
  updateMyTable(
    data: { myColumn: "DbNull" },
    where: { id: "3e48e900-20f3-4cce-899e-f6323525645d" }
  ) {
    id
    myColumn
  }
}

MichalLytek avatar Dec 29 '21 15:12 MichalLytek

Yes, that works @MichalLytek ... but it's pretty weird behavior... setting to null seems more natural and consistent with how it works for other data types

mdesousa avatar Dec 30 '21 20:12 mdesousa

Agree but it's Prisma's design. You can find an issue on their repo and complain about that.

All I can think of here is to introduce yet another generator option like treatNullAs = "DbNull" so that it translates null in query to Prisma.DbNull.

However, it might be working incorrect because JS and GraphQL has weird behavior for null and undefined but will see 👀

MichalLytek avatar Dec 30 '21 20:12 MichalLytek

That makes sense to me @MichalLytek. FYI, Prisma prevents null assignment through type checks (see below). image

I think the issue in typegraphql-prisma is that assigning null is allowed by graphql (it's a valid value for JSON)... but you get an exception when you use it. I think setting to DbNull behind the scenes sounds like the right behavior. For now i'm implementing this middleware to handle this:

const handleNullJson: MiddlewareFn = async (resolver, next) => {
  const { data } = resolver.args;
  if (data.myColumn === null) data.myColumn = 'DbNull';
  return await next();
};

mdesousa avatar Dec 30 '21 21:12 mdesousa

The problem I see with the implementation of such behavior is that GraphQL-JS does not allow for input resolvers. So there's no easy way to apply such transformations. The closest to that is generating a global middleware similar to yours that will try to recursively scan query/mutation inputs and replace the values. I'm not sure what we can do with this issue. I agree that this might be inconvenient as GraphQL allow passing null for optional arguments while Prisma forbids that 😕

MichalLytek avatar Jan 28 '22 14:01 MichalLytek

I think the challenge here is that most databases which support this JSON type would accept null in the field. So to have this limitation due to a single data provider feels a bit restrictive to me and breaks the principle of least surprise

For example, I implement versioned entities which upserts the current table, then inserts into the history table. Since I'm within a transaction block, I should be able trust the data coming back from the entity.upsert method.

// this does not work
const dto = new OrganizationEntity();
const model = await this.$transaction(async ({
    organization, organization_history
}) => {
    const dbo = this.mapper.toDb(dto);

    const model = await organization.upsert({
        create: dbo,
        update: dbo,
        where: { id: dbo.id || -1 } // really annoying, but if the request does not have an ID, I cannot pass undefined or null here, must be a non-nullable value
    });

    await organization_history.create({
        data: model
    });

    return this.mapper.fromDb(model);
});

This forces me to map the model back into my entity class, then back into a prisma-compatible object even though the type definitions for both match

const dto = new OrganizationEntity();
// this works assuming the mappers handle the conversion to & from correctly
const model = await this.$transaction(async ({
    organization, organization_history
}) => {
    const dbo = this.mapper.toDb(dto);

    const model = await organization.upsert({
        create: dbo,
        update: dbo,
        where: { id: dbo.id || -1 } // really annoying, but if the request does not have an ID, I cannot pass undefined or null here, must be a non-nullable value
    });

    const result = this.mapper.fromDb(model);

    await organization_history.create({
        data: this.mapper.toDb(result)
    });

    return result;
});

kn327 avatar Feb 19 '23 16:02 kn327