- 
                Notifications
    
You must be signed in to change notification settings  - Fork 227
 
Description
Currently there are at least 3 critical features missing from the library:
Interface support, Mutation support, and InputObjectType support that integrate with SQLAlchemy models. A last, but nice to have, would be a type that automatically generates all graphene-sqlalchemy models and correctly assigns them the appropriate Interface type based on their model.
Interface is critically important for people using polymorphic database structures. The importance of auto-generated SQLAlchemy Mutations and InputObjectTypes is self explanatory.
a SQLAlchemyInterface should have as its meta fields a name and an SQLAlchemy model, e.g.
class BaseClassModelInterface(SQLAlchemyInterface):
    class Meta:
        name = 'BaseClassNode'
        model = BaseClassModelBecause it will act as a Node elsewhere, in my implementation I have it extend Node (but call super(AbstractNode) to specify it's meta rather than have it be overridden)
def exclude_autogenerated_sqla_columns(model: DeclarativeMeta) -> Tuple[str]:
    # always pull ids out to a separate argument
    autoexclude: List[str] = []
    for col in sqlalchemy.inspect(model).columns:
        if ((col.primary_key and col.autoincrement) or
                (isinstance(col.type, sqlalchemy.types.TIMESTAMP) and
                 col.server_default is not None)):
            autoexclude.append(col.name)
            assert isinstance(col.name, str)
    return tuple(autoexclude)
class SQLAlchemyInterfaceOptions(InterfaceOptions):
    model = None  #
    registry = None  #
    connection = None  #
    id = None  # type: str
class SQLAlchemyInterface(Node):
    @classmethod
    def __init_subclass_with_meta__(
            cls,
            model: DeclarativeMeta = None,
            registry: Registry = None,
            only_fields: Tuple[str] = (),
            exclude_fields: Tuple[str] = (),
            connection_field_factory: UnsortedSQLAlchemyConnectionField = default_connection_field_factory,
            **options
    ):
        _meta = SQLAlchemyInterfaceOptions(cls)
        _meta.name = f'{cls.__name__}Node'
        autoexclude_columns = exclude_autogenerated_sqla_columns(model=model)
        exclude_fields += autoexclude_columns
        assert is_mapped_class(model), (
            "You need to pass a valid SQLAlchemy Model in " '{}.Meta, received "{}".'
        ).format(cls.__name__, model)
        if not registry:
            registry = get_global_registry()
        assert isinstance(registry, Registry), (
            "The attribute registry in {} needs to be an instance of "
            'Registry, received "{}".'
        ).format(cls.__name__, registry)
        sqla_fields = yank_fields_from_attrs(
            construct_fields(
                obj_type=cls,
                model=model,
                registry=registry,
                only_fields=only_fields,
                exclude_fields=exclude_fields,
                connection_field_factory=connection_field_factory
            ),
            _as=Field
        )
        if not _meta:
            _meta = SQLAlchemyInterfaceOptions(cls)
        _meta.model = model
        _meta.registry = registry
        connection = Connection.create_type(
            "{}Connection".format(cls.__name__), node=cls)
        assert issubclass(connection, Connection), (
            "The connection must be a Connection. Received {}"
        ).format(connection.__name__)
        _meta.connection = connection
        if _meta.fields:
            _meta.fields.update(sqla_fields)
        else:
            _meta.fields = sqla_fields
        _meta.fields['id'] = graphene.GlobalID(cls, description="The ID of the object.")
        super(AbstractNode, cls).__init_subclass_with_meta__(_meta=_meta, **options)
    @classmethod
    def Field(cls, *args, **kwargs):  # noqa: N802
        return NodeField(cls, *args, **kwargs)
    @classmethod
    def node_resolver(cls, only_type, root, info, id):
        return cls.get_node_from_global_id(info, id, only_type=only_type)
    @classmethod
    def get_node_from_global_id(cls, info, global_id, only_type=None):
        try:
            node: DeclarativeMeta = info.context.get('session').query(cls._meta.model).filter_by(id=global_id).one_or_none()
            return node
        except Exception:
            return None
    @classmethod
    def from_global_id(cls, global_id):
        return global_id
    @classmethod
    def to_global_id(cls, type, id):
        return id
    @classmethod
    def resolve_type(cls, instance, info):
        if isinstance(instance, graphene.ObjectType):
            return type(instance)
        graphene_model = get_global_registry().get_type_for_model(type(instance))
        if graphene_model:
            return graphene_model
        else:
            raise ValueError(f'{instance} must be a SQLAlchemy model or graphene.ObjectType')A mutation should take as its meta arguments the SQLAlchemy Model, it's CRUD operation . (Create Edit or Delete), and the graphene structure of its response (Output type)
class CreateFoos(SQLAlchemyMutation):
    class Arguments:
        foos = graphene.Argument(graphene.List(FooInput))
    class Meta:
        create = True
        model = FooModel
        structure = graphene.Listclass SQLAlchemyMutation(graphene.Mutation):
    @classmethod
    def __init_subclass_with_meta__(cls, model=None, create=False,
                                    delete=False, registry=None,
                                    arguments=None, only_fields=(),
                                    structure: Type[Structure] = None,
                                    exclude_fields=(), **options):
        meta = SQLAlchemyMutationOptions(cls)
        meta.create = create
        meta.model = model
        meta.delete = delete
        if arguments is None and not hasattr(cls, "Arguments"):
            arguments = {}
            # don't include id argument on create
            if not meta.create:
                arguments['id'] = graphene.ID(required=True)
            # don't include input argument on delete
            if not meta.delete:
                inputMeta = type('Meta', (object,), {
                    'model': model,
                    'exclude_fields': exclude_fields,
                    'only_fields': only_fields
                })
                inputType = type(cls.__name__ + 'Input',
                                 (SQLAlchemyInputObjectType,),
                                 {'Meta': inputMeta})
                arguments = {'input': inputType(required=True)}
        if not registry:
            registry = get_global_registry()
        output_type: ObjectType = registry.get_type_for_model(model)
        if structure:
            output_type = structure(output_type)
        super(SQLAlchemyMutation, cls).__init_subclass_with_meta__(_meta=meta, output=output_type, arguments=arguments, **options)
    @classmethod
    def mutate(cls, info, **kwargs):
        session = get_session(info.context)
        with session.no_autoflush:
            meta = cls._meta
            model = None
            if meta.create:
                model = meta.model(**kwargs['input'])
                session.add(model)
            else:
                model = session.query(meta.model).filter(meta.model.id == kwargs['id']).first()
            if meta.delete:
                session.delete(model)
            else:
                def setModelAttributes(model, attrs):
                    relationships = model.__mapper__.relationships
                    for key, value in attrs.items():
                        if key in relationships:
                            if getattr(model, key) is None:
                                # instantiate class of the same type as
                                # the relationship target
                                setattr(model, key,
                                        relationships[key].mapper.entity())
                            setModelAttributes(getattr(model, key), value)
                        else:
                            setattr(model, key, value)
                setModelAttributes(model, kwargs['input'])
            session.commit()
            return model
    @classmethod
    def Field(cls, *args, **kwargs):
        return graphene.Field(cls._meta.output,
                              args=cls._meta.arguments,
                              resolver=cls._meta.resolver)an SQLAlchemy InputObjectType should introspect the sqla model and autogenerate fields to select based upon and set the appropriate field data type:
e.g.
class Bar(SQLAlchemyInputObjectType):
    class Meta:
        model = BarModel
        exclude_fields = ( 'polymorphic_discriminator', 'active', 'visible_id')
```python
class SQLAlchemyInputObjectType(InputObjectType):
    @classmethod
    def __init_subclass_with_meta__(
            cls,
            model=None,
            registry=None,
            skip_registry=False,
            only_fields=(),
            exclude_fields=(),
            connection=None,
            connection_class=None,
            use_connection=None,
            interfaces=(),
            id=None,
            connection_field_factory=default_connection_field_factory,
            _meta=None,
            **options
    ):
        autoexclude = []
        # always pull ids out to a separate argument
        for col in sqlalchemy.inspect(model).columns:
            if ((col.primary_key and col.autoincrement) or
                    (isinstance(col.type, sqlalchemy.types.TIMESTAMP) and
                     col.server_default is not None)):
                autoexclude.append(col.name)
        if not registry:
            registry = get_global_registry()
        sqla_fields = yank_fields_from_attrs(
            construct_fields(cls, model, registry, only_fields, exclude_fields + tuple(autoexclude), connection_field_factory),
            _as=Field,
        )
        # create accessor for model to be retrieved for querying
        cls.sqla_model = model
        if use_connection is None and interfaces:
            use_connection = any(
                (issubclass(interface, Node) for interface in interfaces)
            )
        if use_connection and not connection:
            # We create the connection automatically
            if not connection_class:
                connection_class = Connection
            connection = connection_class.create_type(
                "{}Connection".format(cls.__name__), node=cls
            )
        if connection is not None:
            assert issubclass(connection, Connection), (
                "The connection must be a Connection. Received {}"
            ).format(connection.__name__)
        for key, value in sqla_fields.items():
            if not (isinstance(value, Dynamic) or hasattr(cls, key)):
                setattr(cls, key, value)
        super(SQLAlchemyInputObjectType, cls).__init_subclass_with_meta__(**options)