Tech stakcs
"@nestjs/apollo": "^10.0.19"
"@nestjs/graphql": "^10.0.21"
"typeorm": "^0.3.7"
"graphql": "^16.5.0"
"@graphql-tools/stitch": "^8.7.6"
hasura graphql engine: 2.9.0
I am using NestJS with Apollo as GraphQL backend and code first approach was used. I am using TypeORM for Postgres db. In the NestJS backend, custom mutations were implemented for CRUD operation on table Todo in db.
Besides the backend I use Hasura to expose automatically generated GraphQL queries for read-only operation on the table. In order to return numeric type value as string by Hasura, default variable HASURA_GRAPHQL_STRINGFY_NUMERIC_TYPES=true was used. The problem here is that this variable sets Hasura to returns double precision type as string as well.
I used schema stitching to stitch Hasura schema to Apollo schema and expose the combined proxy schema via NestJS backend. Mutations implemented in Apollo and Hasura Queries perform operation on the same table.
TypeORM Entity was created automatically using typeorm-model-generator and GraphQLJSON was set in @Field() GraphQL decorator to return jsonb and interval types as JSON.
The entity has the following fields.
@ObjectType()
export class Todo {
@Field(() => Int)
@PrimaryGeneratedColumn({ type: "integer", name: "id" })
id: number;
@Field(() => Float, { nullable: true })
@Column("double precision", {
name: "quantity",
nullable: true,
precision: 53,
})
quantity: number | null;
@Field(() => GraphQLJSON, { nullable: true })
@Column("jsonb", { name: "custom_data", nullable: true })
customData: object | null;
@Field(() => GraphQLJSON, { nullable: true })
@Column("interval", {
name: "duration",
nullable: true,
default: () => "'00:05:00'",
})
duration: any | null;
@Field({ nullable: true })
@Column("timestamp with time zone", {
name: "created_at",
nullable: true,
default: () => "CURRENT_TIMESTAMP",
})
createdAt: Date | null;
}
In app.module.ts file, remote schema was stitched as follow.
const createRemoteSchema = async (url: string) => {
const executor = async ({ document, variables }) => {
const query = print(document);
const fetchResult = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json',},
body: JSON.stringify({ query, variables }),
});
return fetchResult.json();
};
return {
schema: await introspectSchema(executor),
executor: executor,
};
};
...
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const hasuraSchema = await createRemoteSchema(url);
return {
driver: ApolloDriver,
debug: false,
playground: true,
introspection: true,
autoSchemaFile: true,
context: ({ req }) => ({ headers: req.headers }),
transformSchema: async (schema: GraphQLSchema) => {
return stitchSchemas({ subschemas: [hasuraSchema, apolloSchema] });
},
formatError: (error) => errorFormatter(error),
};
},
}),
...
When the schemas are stitching at NestJS backend startup, the following warnings appear.
Definitions of field "Todo.duration" implement inconsistent named types across subschemas. This will be an automatic error in future versions. To disable this warning or elevate it to an error, set typeMergingOptions.validationScopes['Todo.duration'].validationLevel = "error|off"
Definitions of field "Todo.quantity" implement inconsistent named types across subschemas. This will be an automatic error in future versions. To disable this warning or elevate it to an error, set typeMergingOptions.validationScopes['Todo.quantity'].validationLevel = "error|off"
Definitions of field "Todo.createdAt" implement inconsistent named types across subschemas. This will be an automatic error in future versions. To disable this warning or elevate it to an error, set typeMergingOptions.validationScopes['Todo.createdAt'].validationLevel = "error|off"
What I figured out is that Hasura returns cratedAt as timestamptz while Apollo returns is as DateTime scalar and I also want to return interval type as JSON format.
So I used TransformObjectFields to transform the types to default GraphQL types as follow.
const createRemoteSchema = async (url: string) => {
const executor = async ({ document, variables }) => {
const query = print(document);
const fetchResult = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json',},
body: JSON.stringify({ query, variables }),
});
return fetchResult.json();
};
return {
schema: await introspectSchema(executor),
executor: executor,
transforms: [
new TransformObjectFields((typename, fieldName, fieldConfig) => {
let newFieldConfig;
// Map timestamptz to GraphQLDateTime
if (fieldConfig.type.toString() === 'timestamptz') {
newFieldConfig = fieldConfig;
newFieldConfig.type = GraphQLDateTime;
return newFieldConfig;
}
// Map interval to GraphQLJSON
if (fieldConfig.type.toString() === 'interval') {
newFieldConfig = fieldConfig;
newFieldConfig.type = GraphQLJSON;
return newFieldConfig;
}i
return fieldConfig;
}),
],
};
};
Warning about createdAt(timestamp) field and duration(interval) disappeared. However there are still warnings about quantity(double precision) and customData(jsonb) fields. I also tried typeMergingOptions and onTypeConflict options, but it didn't work.
transformSchema: async (schema: GraphQLSchema) => {
return stitchSchemas({
subschemas: [hasuraSchema, apolloSchema],
typeMergingOptions: {
typeCandidateMerger: (candidates) => candidates[0],
typeDescriptionsMerger: (candidates) => candidates[0].type.description,
fieldConfigMerger: (candidates) => candidates[0].fieldConfig,
inputFieldConfigMerger: (candidates) => candidates[0].inputFieldConfig,
enumValueConfigMerger: (candidates) => candidates[0].enumValueConfig,
},
});
},
transformSchema: async (schema: GraphQLSchema) => {
return stitchSchemas({
subschemas: [hasuraSchema, apolloSchema],
onTypeConflict: (hasuraField, apolloField) => apolloField,
});
},
Here I have some questions.
Q1. Am I using TransformObjectFields() correctly to transform the types?
Q2. quantity(double precision) field is stringified because of HASURA_GRAPHQL_STRINGFY_NUMERIC_TYPES=true that I mentioned above. So when I try to get the field from Hasura query, it throws the following response.
"message": "Float cannot represent non numeric value: \"182.24\"",
"locations": [
{
"line": 6,
"column": 5
}
],
Is there a way to solve this problem?
Q3. I can get the customData(jsonb) as JSON format, but the warning still appears. Is there a way to fix this error instead of set it off?
Q4. I want to return interval type data into JSON format. I tried with 4 different intervalstyles (sql_standard , postgres , postgres_verbose, iso_8601 ), result changed accordingly, but it didn't parse it as JSON format. Is there a way to return interval type as JSON format?