pythongraphqlgraphene-python

Creating Dynamic Schema on Runtime Graphene


I almost spent 3 days to find a way for creating a dynamic schema in python graphene. the only related result I could find is the below link: https://github.com/graphql-python/graphene/blob/master/graphene/types/dynamic.py But I couldn't find any documentation for it.

The whole idea is to create a dynamic schema. I want to provide a GraphQL compatible API that makes users able to query my contents even if Models are not defined in the code. In other words, I want to create Models on the fly. I have no idea about what shall I do.

It would be a great favor if you can provide an example for that.

Update :

My Project is a Headless CMS which has a feature that users can create their own content types and I want to provide a GraphQL interface to make everything easier and more flexible.

Here is example of my Content Types in DB :

{
  "id": "author",
  "name": "Book Author",
  "desc": "",
  "options":[
    {
      "id": "author_faname",
      "label": "Sample Sample",
      "type": "text",
      "required": true,
      "placeholder":"One Two Three Four"
    },
    {
      "id": "author_enname",
      "label": "Sample label",
      "type": "text",
      "required": true,
      "placeholder":"Sample Placeholder"
    }
  ]
}

And Here is Stored content in DB based on that content type :

{
  "id": "9rqgbrox10",
  "content_type": "author",
  "data":{
    "author_fname":"Jimmy",
    "author_ename":"Hello"
  }
}

Now as my Models are not declared in Code and they are completely in DB, I want to make my schemas on the fly and I don't know what is best the solution for this. I know there should be a way because the other Headless CMS Projects are providing this.

Thanks in advance!


Solution

  • Basically, schema is created something like this:

    class MyType(graphene.ObjectType):
        something = graphene.String()
    
    class Query(graphene.ObjectType):
        value = graphene.Field(MyType)
    
    schema = graphene.Schema(query=Query, types=[MyType])
    

    First, in order to add some kind of dynamics, you will most likely want to wrap the above code in a function like create_schema().

    Then, when you want to dynamically create a class during runtime, the above code can be rewritten like this:

    def create_schema():
        MyType = type('MyType', (graphene.ObjectType,), {
            'something': graphene.String(),
        })
    
        Query = type('Query', (graphene.ObjectType,), {
            'value': graphene.Field(MyType),
        })
    
        return graphene.Schema(query=Query, types=[MyType])
    

    For your example it could look something like this:

    def make_resolver(record_name, record_cls):
        def resolver(self, info):
            data = ...
            return record_cls(...)
        resolver.__name__ = 'resolve_%s' % record_name
        return resolver
    
    def create_schema(db):
        record_schemas = {}
        for record_type in db.get_record_types():
            classname = record_type['id'].title()  # 'Author'
            fields = {}
            for option in record_type['options']:
                field_type = {
                    'text': graphene.String,
                    ...
                }[option['type']
                fields[option['id']] = field_type()  # maybe add label as description?
            rec_cls = type(
                classname,
                (graphene.ObjectType,), 
                fields,
                name=record_type['name'],
                description=record_type['desc'],
            )
            record_schemas[record_type['id']] = rec_cls
    
        # create Query in similar way
        fields = {}
        for key, rec in record_schemas:
            fields[key] = graphene.Field(rec)
            fields['resolve_%s' % key] = make_resolver(key, rec)
        Query = type('Query', (graphene.ObjectType,), fields)
    
        return graphene.Schema(query=Query, types=list(record_schemas.values()))
    

    Note that if you try to insert new fields into already existing class, like this - MyType.another_field = graphene.String(), then it won't work: that is because when graphene.ObjectType class is instantiated, all its fields are recorded in self._meta.fields OrderedDict. And updating it is not as straightforward as just MyType._meta.fields['another_field'] = thefield - see the code of graphene.ObjectType.__init_subclass_with_meta__ for details.

    So if your schema is dynamically changed then it might be better to fully re-create it from scratch than to patch it.