Created
June 2, 2022 16:43
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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