Alexis.Rolland
Alexis.Rolland

Reputation: 6353

Generate Graphene Mutation Inputs from SQLAlchemy Class Attributes

I currently have the following mutation defined for my project:

My class PlanetAttribute is used to define Graphene Fields used as inputs of my mutations

class PlanetAttribute:
    name = graphene.String(required=True, description="Name of the planet.")
    rotation_period = graphene.String(default_value="unknown", description="Rotation period of the planet.")
    orbital_period = graphene.String(default_value="unknown", description="Orbital period of the planet.")
    diameter = graphene.String(default_value="unknown", description="Diameter of the planet.")
    climate = graphene.String(default_value="unknown", description="Climate period of the planet.")
    gravity = graphene.String(default_value="unknown", description="Gravity of the planet.")
    terrain = graphene.String(default_value="unknown", description="Terrain of the planet.")
    surface_water = graphene.String(default_value="unknown", description="Surface water of the planet.")
    population = graphene.String(default_value="unknown", description="Population of the planet.")
    url = graphene.String(default_value="unknown", description="URL of the planet in the Star Wars API.")

My class CreatePlanetInput is used to define the Graphene input object type. Note that it inherits its attributes from the PlanetAttribute class defined above.

class CreatePlanetInput(graphene.InputObjectType, PlanetAttribute):
    """Arguments to create a planet."""
    pass

My class CreatePlanet is my Graphene mutation class which takes the CreatePlanetInput class as arguments.

class CreatePlanet(graphene.Mutation):
    """Create a planet."""
    planet = graphene.Field(lambda: Planet, description="Planet created by this mutation.")

    class Arguments:
        input = CreatePlanetInput(required=True)

    def mutate(self, info, input):
        data = utils.input_to_dictionary(input)
        data['created'] = datetime.utcnow()
        data['edited'] = datetime.utcnow()

        planet = ModelPlanet(**data)
        db_session.add(planet)
        db_session.commit()

        return CreatePlanet(planet=planet)

Instead of declaring mutation inputs manually in the PlanetAttribute class, I would rather generate them dynamically from my SQLALchemy class ModelPlanet which is defined as below:

class ModelPlanet(Base):
    """Planet model."""

    __tablename__ = 'planet'

    id = Column('id', Integer, primary_key=True, doc="Id of the person.")
    name = Column('name', String, doc="Name of the planet.")
    rotation_period = Column('rotation_period', String, doc="Rotation period of the planet.")
    orbital_period = Column('orbital_period', String, doc="Orbital period of the planet.")
    diameter = Column('diameter', String, doc="Diameter of the planet.")
    climate = Column('climate', String, doc="Climate period of the planet.")
    gravity = Column('gravity', String, doc="Gravity of the planet.")
    terrain = Column('terrain', String, doc="Terrain of the planet.")
    surface_water = Column('surface_water', String, doc="Surface water of the planet.")
    population = Column('population', String, doc="Population of the planet.")
    created = Column('created', String, doc="Record created date.")
    edited = Column('edited', String, doc="Record last updated date.")
    url = Column('url', String, doc="URL of the planet in the Star Wars API.")

    peopleList = relationship(ModelPeople, backref='planet')

How would you proceed?

Note that I have also posted the question here: https://github.com/graphql-python/graphene-sqlalchemy/issues/112

Upvotes: 3

Views: 1746

Answers (1)

Fedalto
Fedalto

Reputation: 1567

I solved creating this class:

from graphene.types.utils import yank_fields_from_attrs
from graphene.utils.subclass_with_meta import SubclassWithMeta_Meta
from graphene_sqlalchemy.registry import get_global_registry
from graphene_sqlalchemy.types import construct_fields

class SQLAlchemyInputObjectType(graphene.InputObjectType):
    @classmethod
    def __init_subclass_with_meta__(  # pylint: disable=arguments-differ
        cls, model=None, registry=None, only_fields=(), exclude_fields=(),
        optional_fields=(), **options
    ):
        if not registry:
            registry = get_global_registry()

        sqla_fields = yank_fields_from_attrs(
            construct_fields(model, registry, only_fields, exclude_fields),
            _as=graphene.Field,
        )

        for key, value in sqla_fields.items():
            if key in optional_fields:
                type_ = value.type if isinstance(
                    value.type, SubclassWithMeta_Meta) else value.type.of_type
                value = type_(
                    description=value.description
                )
            setattr(cls, key, value)

        super(SQLAlchemyInputObjectType, cls).__init_subclass_with_meta__(
            **options
        )

You can use it with:

class CreatePlanetInput(SQLAlchemyInputObjectType):
    class Meta:
        model = ModelPlanet
        exclude_fields = ('id', )

I tried to make it behave similar with SQLAlchemyObjectType, so only_fields and exclude_fields should work as expected.

I also added an optional_fields that makes it not required, good for an update mutation.

It does handle the simple cases, like scalar fields. Relationships, like peopleList, you need have to declare manually.

Upvotes: 5

Related Questions