Skip to content

Non-nullability for mongoose fields that have a default value and other considerations #261

@toverux

Description

@toverux

Currently, graphql-compose-mongoose yields a non-nullable type for schema fields that have a { required: true } option.
Most of the time (there is a caveat as we'll see below), it is very useful that this configuration is converted into a non-nullable field in the GraphQL input type.

Now, there is also { default: <some value> }. Such a configuration sets the default value for a field when a document is created, but, documents that don't have this property set in the database (ex. they've been created before this configuration option was set or were altered by hand) will also have the value hydrated when queried through mongoose (given that you've not made your query enabling mongoose's lean document mode).

In fact, a field that is configured with a default value will in fact, never appear null when accessed from the GraphQL API, even if the MongoDB document does not have that property set.

Given all of the above, I see two problems with how graphql-compose-mongoose handles nullability:

  • A field that have a { required: true, default: someValue } configuration will be made non-nullable in the GraphQL schema, while it is actually not necessary to provide a value here, since mongoose will handle it. But that's not that bad. Patching the type composers by hand for this is not too cumbersome.

  • More problematic, a field that has a { default: someValue } configuration is currently not being given a non-nullable output type.

I am working in a codebase with auto-generation of TypeScript types based on the GraphQL schema of the server, and we use null-strict mode.
Working with all those nullable fields everywhere (the schema is pretty big) are a pain in the *** and it's not great for self-documentation purposes :D

What we do today is that we're patching the type composer by hand to make the appropriate fields non-nullable. But

  • it's painful to do,
  • in the future we won't necessarily remember to change the schema if the mongoose schema changes,
  • it's just terrible-looking code given the current API.

I'll give a simple example here, that's the code I was working on.

export const moduleConfigTunnelStepSchema = new Schema({
    // ...
    analytics: {
        choiceReducer: {
            isEnabled: { type: Boolean, default: false },
            stateProperties: { type: [String], default: [] }
        }
    }
});

With this schema, if I query any document, even if it doesn't have those fields defined, there is no problem, I don't get a single null. Interestingly, default is 'recursive' in a way: I didn't have to provide an explicit default value for analytics nor choiceReducer.
(Maybe the compiled shema in the mongoose model object has that "recursive default" information, that would be useful to exploit it in the library.)
Now I will always get at least:

{
  "analytics": {
    "choiceReducer": {
      "isEnabled": false,
      "stateProperties": []
    }
  }
}

However, based on this configuration, graphql-compose-mongoose generates these fields :

type ModuleConfigTunnelStep {
  analytics: ModuleConfigStepsAnalytics # should be non-null
}

type ModuleConfigStepsAnalytics {
  choiceReducer: ModuleConfigStepsAnalyticsChoiceReducer # should be non-null
}

type ModuleConfigStepsAnalyticsChoiceReducer {
  isEnabled: Boolean # should be non-null
  stateProperties: [String] # should be a non-null list, and i'd like it to be of non-null strings, ie. [String!]!
}

input ModuleConfigStepsInput {
  analytics: ModuleConfigStepsAnalyticsInput # ok
}

input ModuleConfigStepsAnalyticsInput {
  choiceReducer: ModuleConfigStepsAnalyticsChoiceReducerInput # ok
}

input ModuleConfigStepsAnalyticsChoiceReducerInput {
  isEnabled: Boolean # ok
  stateProperties: [String] # i'd like this to be a nullable list of non-nullable strings, ie. [String!]
}

To fix those types according to our more rigorous nullability annotations rules, we have to do all of this horrendous code (long form, fortunately we've made some utility functions to shorten it a bit but that's very similar):

tunnelStepTc.makeFieldNonNull('analytics');
tunnelStepTc.getFieldOTC('analytics').makeFieldNonNull('choiceReducer');
tunnelStepTc.getFieldOTC('analytics').getFieldOTC('choiceReducer').makeFieldNonNull('isEnabled');
tunnelStepTc.getFieldOTC('analytics').getFieldOTC('choiceReducer').extendField('stateProperties', {
    type: tunnelStepTc.getFieldOTC('analytics').getFieldOTC('choiceReducer').getFieldTC('stateProperties').getTypeNonNull().getTypePlural().getTypeNonNull()
});

tunnelStepTc.getInputTypeComposer().getFieldITC('analytics').getFieldITC('choiceReducer').extendField('stateProperties', {
    type: tunnelStepTc.getInputTypeComposer().getFieldITC('analytics').getFieldITC('choiceReducer').getFieldTC('stateProperties').getTypeNonNull().getTypePlural()
});

To conclude, we have too much nullability issues with compose libraries. I think that it should either be more "non-nullable" by default on some things (ex. lists with nullable elements are generally rare but compose only produces this) and/or better convert the meaning of mongoose schemas, and/or provide more declarative APIs to configure the nullability of fields with more ease than the current imperative API.

Sorry for that long issue. I just wanted to share my feelings about this as it kinda defeats the usefulness and goal of compose-mongoose for us. But I know that's a delicate subject that requires deep thinking, and that mongoose doesn't really helps either.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions