Skip to content

Instantly share code, notes, and snippets.

@maaft
Created June 2, 2022 16:43
Show Gist options
  • Save maaft/5d67947bc31ad161fb451dc5be7a5c50 to your computer and use it in GitHub Desktop.
Save maaft/5d67947bc31ad161fb451dc5be7a5c50 to your computer and use it in GitHub Desktop.
This will mutate an extracted GraphQL Schema from Hasura to add client-side custom type support for JSONB columns
from pathlib import Path
from typing import Optional, Set
from gql import gql
from graphql import GraphQLEnumType, GraphQLFieldMap, GraphQLInputObjectType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, extend_schema, build_ast_schema
from graphql.utilities import print_schema
#############
# HOWTO
# - api.graphql: schema extracted from hasura API
# - jsonb scalars require following comments: "type: <ExpectedTypeName>"
# - custom types can be specified using hasura actions. Important: This Script requires <ExpectedTypeName>Input types for all complex custom types! Example:
# // our type definition that we want to store in our JSONB column
# type ExpectedTypeName {
# foos: [Foo!]!
# }
# type Foo {
# lol: String
# }
# // A matching input type definition for our JSONB column
# input ExpectedTypeNameInput {
# foos: [FooInput!]!
# }
# input FooInput {
# lol: String
# }
# // convenience wrapper if you want to generate multiple custom types
# type TypeGenType {
# a: ExpectedTypeName!
# b: AnotherCustomType!
# }
# input TypeGenInputType {
# a: ExpectedTypeNameInput!
# b: AnotherCustomTypeInput!
# }
# // wihout this "typegen-action", no custom types will be exposed via the GraphQL Schema
# type Mutation {
# typegen(input: TypeGenInputType!): TypeGenType
# }
# - if the custom type is not added via Hasura Actions like described above, a custom scalar with that name will be added to the schema
# - the client will be responsible to handle that scalar
txt = Path('api.graphql').read_text()
ast = gql(txt)
addedScalars: Set[str] = set([])
schema = build_ast_schema(ast)
knownTypes: Set[str] = set(["Float", "String", "Int"])
for t in schema.type_map.values():
if isinstance(t, (GraphQLObjectType, GraphQLInputObjectType, GraphQLEnumType, GraphQLScalarType)):
knownTypes.add(t.name)
for typename, t in schema.type_map.items():
if typename == "jsonb_comparison_exp":
continue
if isinstance(t, (GraphQLObjectType, GraphQLInputObjectType)):
inputType = isinstance(t, GraphQLInputObjectType)
fields: GraphQLFieldMap = t.fields
for name, f in fields.items():
sType: Optional[GraphQLScalarType] = None
if isinstance(f.type, GraphQLScalarType):
sType = f.type
elif isinstance(f.type, GraphQLNonNull) and isinstance(f.type.of_type, GraphQLScalarType):
sType = f.type.of_type
if sType is not None and "jsonb" in sType.name:
if f.description is None or (f.description is not None and "type:" not in f.description):
raise Exception(
f"jsonb fields must have 'type: <typename>' comment! field '{name}' on {'Input' if inputType else ''}Type '{t.name}'")
if f.description.index("type:") != 0:
raise Exception(
"format must be 'type: <typename>'")
wantedType = f.description[len(
"type:"):].strip()
addScalar = False
isArray = False
isNonNullableArray = False
isNonNullableInner = False
if "[" in wantedType:
isArray = True
arrayType = wantedType[1:]
if arrayType[-1] == "!":
isNonNullableArray = True
arrayType = arrayType[:-1]
arrayType = arrayType[:-1]
if arrayType[-1] == "!":
arrayType = arrayType[:-1]
isNonNullableInner = True
if arrayType not in knownTypes:
addScalar = True
wantedType = arrayType
else:
if wantedType not in knownTypes:
addScalar = True
if addScalar:
addedScalars.add(wantedType)
else:
if isinstance(t, GraphQLInputObjectType) and isinstance(schema.get_type(wantedType), GraphQLObjectType):
wantedType = wantedType+"Input"
kwargs = sType.to_kwargs()
kwargs["name"] = wantedType
newType = GraphQLScalarType(**kwargs)
if isNonNullableInner:
newType = GraphQLNonNull(newType)
if isArray:
newType = GraphQLList(newType)
if isNonNullableArray:
newType = GraphQLNonNull(newType)
f.type = newType
if len(addedScalars) > 0:
added_scalars_str = ""
for s in addedScalars:
added_scalars_str += f"scalar {s}\n"
extended = extend_schema(schema, gql(added_scalars_str))
else:
extended = schema
with open("api.graphql", "w") as f:
f.write(print_schema(extended))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment