Repository navigation
Enhancements: SQLA Interface, Mutation, InputObjectType and auto schema generation #226
Description
Activity
Lastly, a nice to have feature would auto generate the schema and query for you, given a list of models, interfaces, excluded models, and a default node interface (because we'll usually be overriding Node with a custom node that provides the database ID anyways)
e.g.
class Query(SQLAlchemyAutoSchemaFactory): class Meta: interfaces = (FooInterface, BarInterface,) models = (*foos, *bars, *others, *associatives) # associatives are for AssociationObjectTypes, need to create the graphene models for these to work node_interface = CustomNode excluded_models = (TrackedEntityModel, BaseControlledVocabulary)
class SQLAlchemyAutoSchemaFactory(graphene.ObjectType): @staticmethod def set_fields_and_attrs(klazz, node_model, field_dict): _name = to_snake_case(node_model.__name__) field_dict[f'all_{(pluralize_name(_name))}'] = FilteredConnectionField(node_model) field_dict[_name] = node_model.Field() # log.info(f'interface:{node_model.__name__}') setattr(klazz, _name, node_model.Field()) setattr(klazz, "all_{}".format(pluralize_name(_name)), FilteredConnectionField(node_model)) @classmethod def __init_subclass_with_meta__( cls, interfaces: Tuple[Type[SQLAlchemyInterface]] = (), models: Tuple[Type[DeclarativeMeta]] = (), excluded_models: Tuple[Type[DeclarativeMeta]] = (), node_interface: Type[Node] = Node, default_resolver: ResolveInfo = None, _meta=None, **options ): if not _meta: _meta = ObjectTypeOptions(cls) fields = OrderedDict() for interface in interfaces: if issubclass(interface, SQLAlchemyInterface): SQLAlchemyAutoSchemaFactory.set_fields_and_attrs(cls, interface, fields) for model in excluded_models: if model in models: models = models[:models.index(model)] + models[models.index(model) + 1:] possible_types = () for model in models: model_name = model.__name__ _model_name = to_snake_case(model.__name__) if hasattr(cls, _model_name): continue if hasattr(cls, "all_{}".format(pluralize_name(_model_name))): continue for iface in interfaces: if issubclass(model, iface._meta.model): model_interface = (iface,) break else: model_interface = (CustomNode,) _node_class = type(model_name, (SQLAlchemyObjectType,), {"Meta": {"model": model, "interfaces": model_interface, "only_fields": []}}) fields["all_{}".format(pluralize_name(_model_name))] = FilteredConnectionField(_node_class) setattr(cls, "all_{}".format(pluralize_name(_model_name)), FilteredConnectionField(_node_class)) fields[_model_name] = node_interface.Field(_node_class) setattr(cls, _model_name, node_interface.Field(_node_class)) possible_types += (_node_class,) if _meta.fields: _meta.fields.update(fields) else: _meta.fields = fields _meta.schema_types = possible_types super(SQLAlchemyAutoSchemaFactory, cls).__init_subclass_with_meta__(_meta=_meta, default_resolver=default_resolver, **options) @classmethod def resolve_with_filters(cls, info: ResolveInfo, model: Type[SQLAlchemyObjectType], **kwargs): query = model.get_query(info) for filter_name, filter_value in kwargs.items(): model_filter_column = getattr(model._meta.model, filter_name, None) if not model_filter_column: continue if isinstance(filter_value, SQLAlchemyInputObjectType): filter_model = filter_value.sqla_model q = FilteredConnectionField.get_query(filter_model, info, sort=None, **kwargs) # noinspection PyArgumentList query = query.filter(model_filter_column == q.filter_by(**filter_value)) else: query = query.filter(model_filter_column == filter_value) return query
Hello @maquino1985 ,
Thanks for starting these 4 enhancement proposals. There is quite a bit to unpack and discuss about each of those enhancements. Since those enhancements seem to be fairly independent from one another, can you create separate issues in order for the discussions to be productive and clear?
For example, I agree with you that there is potential for
graphene-sqlalchemyto help with mutations but it is not obvious to me why your suggested implementation is the right solution. You can see some of my questions here. In addition to those questions, I would also like to hear your thoughts on input validation since it's very uncommon for a mutation to save data without validating user inputs.Given that we have only a couple of semi-active maintainers for this project, I would also suggest that you start with the one issue you care the most about so we can make good progress on it instead making slower progress on all of those. There is only so much time per week the maintainers can dedicate to this project.
I hope you understand.
Best.Reacted by Christoph ZwerschkeHi @jnak , can you elaborate on your question about input validation? Other than requiring specific inputs of specific types in the Mutation Arguments, input validation should always occur in the SQLAlchemy model and/or at the database level upon updating or saving any data. I
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related topics referencing this issue.
- locked as resolved and limited conversation to collaborators
on Feb 25, 2023
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.
Because 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)
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)
an SQLAlchemy InputObjectType should introspect the sqla model and autogenerate fields to select based upon and set the appropriate field data type:
e.g.