Why the flatData's fields are always string

I use GraphQL Codgen to generate schema types.

Here is a FlatDataDto(RecipesFlatDataDto):

/** The structure of the flat recipes data type. */
export type RecipesFlatDataDto = {
  __typename?: 'RecipesFlatDataDto';
  author?: Maybe<Array<Authors>>;
  bio?: Maybe<Scalars['String']['output']>;
  body?: Maybe<Scalars['String']['output']>;
  mainImage?: Maybe<Array<Asset>>;
  nutrition?: Maybe<Array<RecipesDataNutritionChildDto>>;
  publishedAt?: Maybe<Scalars['Instant']['output']>;
  slug?: Maybe<Scalars['String']['output']>;
  tag?: Maybe<Array<RecipesDataTagChildDto>>;
  title?: Maybe<Scalars['String']['output']>;
};

I also verify the Introspection result:

For example, the title field type is null.

And, I check the Schema of the recipes’ filed title:

It is required, what’s wrong?
Where can I set it not to be nullable?

Because when you declare a field as required after you have already created content items that do not have a value for the field, the server would throw an error and not return any results at all. So it is very hard to recover from that state.

I’m not sure I understand you, but I made another attempt.

I created a new schema name as test, is has a required notnull field.

The field is still null in the introspection result.

Lets say you execute the following steps:

  1. Create schema
  2. Add non-required field foo
  3. Add content without any value for foo.
  4. Make field required → Futher updates need a value for foo but you have content items with no value for foo in the database.
  5. Query content items using GraphQL

If foo would be required in GraphQL it would return an error and no result.

I don’t know if there is any misunderstanding in my previous expression.

I just want to set the field to not be nullable, so that the field generated by codegen does not need to be nullable (?).

Yes, I understand that, but schema is generated from a server structure that is also used to execute queries. And this would throw an error if you would return a null value for a non-null field.

Oh yes, that is why I want to set a field to required, it will generate a field to non-null, but this seems to not work, can we set fields to no-null?

What is no-null?

This is just how GraphQL and GraphQL Server implementation works, sorry

I’m still doesn’t know what means.

I also a newer to GraphQL, the backend side is also hasn’t read relate code that how to implement it.

For now, I just want to understand how to set a non-null field, thanks.

It is not possible, thats just how GraphQL works:

I try to explain it again (last attempt).

In the server you define a schema like that:

public class DroidType : ObjectGraphType<Droid>
{
  public DroidType()
  {
    Field(
      name = "name",
	  resolver = x => x.Name,
	  type = new NonNullType(new StringType())
	);
  }
}

The field has a

  • Name: The name of the field.
  • Resolver: Used to resolve a value. Could be from the database or just a property from a class (in this case Droid).
  • Type: Defines the type of the field.

The schema has several purposes:

  1. It is used to validate your query.
  2. It is used to expose the schema information to the client, so that you can generate code or just as documentation.
  3. It is used to execute the query.

So in this case a schema is exposed that looks like this:

type Droid {
  name: String!
}

When a query is executed on the server, the resolver is called, which returns a value. As mentioned above it could be from the database or from a class property or basically anything.

This value is validated against the type. In this case, the resolver must return a string, which cannot be null. If the resolver returns a value that does not match to the data type, the query is cancelled and an error is returned.

In Squidex a required field only guarantees that future content items have a value for the field. Therefore it is a validation property. If you already have content items, which have no value for the field or null, then the GraphQL server would return an error.

Lets assume we would be able to mark the field as required, then it would cause other issues:

  1. Some GraphQL client libraries also validate the result that is coming from the server. In this case the client would throw an exception or return an error.

  2. If you would mark your C# or kotlin property as not-null and it would contain a null value (because there are no guarantees at runtime), you would get an error somewhere else, when you actually try to call a method on this null value.

So in short: There is no solution. If you really want to do that, I would just use a little bit of regex and adjust the schema before you put it into a code generator or client.

This explanation saved me, I’m also looking for the backend code on how to implement it, I found the fields have a bool type’s isRequired, which means we have a chance to handle it and render the GraphQL scheme type has a corresponding no-null field.

As for your mentioned issues, that is a data migration problem, not the type problem, right?

First, we should keep the GraphQL type consistent with the Schema field.
Then, we can resolve to protect data integrity.

Maybe I can create the PR, but I need some time to learn it first.

Problem

Yes, but there is no easy solution. First you have to define a data migration strategy. Lets assume we make it only work for the required validation rule, because this is probably the easiest.

For this rule it already depends on the field type. Lets talk about a few example.

  1. String, Number, Boolean: Easy, it is "", 0 and false
  2. Arrays: Easy, it is [].
  3. Json: No idea, could be [], {}, "", 0 or false. It really depends what the user expects. This could be solved with an additional field in the UI, where you have to define something.
  4. Components (a field where you can embed another schema, for example either Article or Blog): No idea, what is a default Article, perhaps it does not even make sense to support this field type.

We only solved it for required fields, because this is the only issue with GraphQL. But you probably want to provide a solution that works fine for other validation rules as well and in this case you have to define a default value, because there is no way to create a default value that matches to a regex pattern.

So you already see, it gets complicated. Now there are 2 different migration strategies. Each of them has major problems:

  1. Enrich the content after it is queried from the database: This is fast and relatively easy to implement, but it has the problem, that the result you see as a developer does not match to the content in the database. Therefore, if you use filters, the result might not be as expected, because you don’t know the actual value in the database. Squidex already has a on-the-fly enrichment for default values and I don’t like it that much, because of this reason.
  2. Update all records in the database. This might take a long time, therefore you need a user interface for it, you have to track progress and so on. Squidex provides this feature via the AP and the CLI, because then no user interface is needed.

We could say: If a field is required and if there is a default value, which is not null, we can make the field non-null in GraphQL, BUT there is a special header which allows you to turn the enrichment off and is needed for some edge cases and this would then destroy the whole feature. You cannot decide per query whether a type is non-null or not.

No, it is the opposite. First you have to ensure that you will never, ever deliver a null value to GraphQL. Then you can make it required. But as mentioned above, I don’t see an easy solution.

The solution in your code

I was also facing this issue and this is how I usually solve it:

Create a clear interface

First I create an easy interface, that decouples Squidex from my application code, for example:

interface Recipe {
	name: string;
	author: string;
	authorBio: string;
	body: string;
	mainImage: string;
}

interface RecipeRepository {
	getAll(): Promise<Recipe[]>;
}

Inside the repository I deal with all these problems.

Perhaps I filter out the results that do not match to my query and log them somewhere and create an alert in my logging sytem, so I can tell the content editors, that someone has forgotten to add the nutrition information to a recipe.

Or I enrich my results with default values, if needed.

Conclusion / Opinion

I think you are right. It would be ideal if Squidex has stronger guarantees for data consistency. But I think it is not worth the effort. Because such a system would be restricted and/or very complicated. And it would not provide you all features. For example I mentioned logging and alerting. Or complex validation rules.

We could add a checkbox to the field settings, which would give you the opportunity to mark a field as non-null in GraphQL and then you are on your own to guarantee that, but we would have to add a big red, warning box next to it.

1 Like

After learning the graphql-dotnet and docs and some examples, such as server/samples, I have a basic understanding about how did GraphQL work in dot net.

Then, I tried to read the Squidex backend source code, in previous studies, I noticed the GraphType First Approach is comfortable with Squidex to implement business, so I search the keyword ObjectGraphType.

Finally, I found the DataFlatGraphType inheritance ObjectGraphType and handled the logic of flatData field.

And the FieldVisitor implements the builder GetGraphType’s logic. I think this line is why the fields in flatData are always string because we assume the type of flatData′ filed is Scalars.String and it actually is StringGraphType.

You are at the right place, the visitor creates fields types + resolvers depending on the type of the field in Squidex. So for StringFields everything is strings obviously and just above that you see a number.

I see more and think that it is more complex, as you said it is hardly work to do.

Currently, I solved it by myself, if I set the field to required, then I can use assert(!) to promise it is not null.

But I also think that is an interesting thing, it is a backlog job.

1 Like