Getting Started¶
Magql-SQLAlchemy is meant to be very simple to use, generating an initial
magql.Schema with no configuration, then taking advantage of Magql’s
ability to modify the schema before finalizing. Therefore, the
Magql Documentation will have most of the information you’ll need.
Defining the Schema¶
All you need is a SQLAlchemy declarative base class with some models defined.
Then pass it to ModelGroup.from_declarative_base(). Most of the code
below is the SQLAlchemy setup. Only the last two lines are where
Magql-SQLAlchemy generates the API and registers it on the schema.
from sqlalchemy import create_engine, ForeignKey
from sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_column, relationship
import magql
from magql_sqlalchemy import ModelGroup
class Model(DeclarativeBase):
pass
class User(Model):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(unique=True)
tasks: Mapped[list["Task"]] = relationship(back_populates="user")
class Task(Model):
__tablename__ = "task"
id: Mapped[int] = mapped_column(primary_key=True)
message: Mapped[str]
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
user: Mapped[User] = relationship(back_populates="tasks")
engine = create_engine("sqlite:///example.db", echo=True)
session = Session(engine)
Model.metadata.create_all(engine)
schema = magql.Schema()
# Generate an API from the models, then register it on the schema.
model_group = ModelGroup.from_declarative_base(Model)
model_group.register(schema)
Executing Queries¶
The resolvers require passing the SQLAlchemy session in the GraphQL execution
context. They expect the context to be a dict, and the session to be on the
sa_session key.
result = schema.execute(
"{ user_item(id: 1) { username } }",
context={"sa_session": session}
)
Generated API¶
With those two lines, the Magql-SQLAlchemy ModelGroup has created a
ModelManager for each model, which generated the following API
operations:
type Query {
task_item(id: Int!): Task
task_list(filter: [[FilterItem!]!], sort: [String!], page: Int, per_page: Int): TaskListResult!
user_item(id: Int!): User
user_list(filter: [[FilterItem!]!], sort: [String!], page: Int, per_page: Int): UserListResult!
search(value: String!): [SearchResult!]!
check_delete(type: String!, id: ID!): CheckDeleteResult
}
type Mutation {
task_create(message: String!, user: Int!): Task!
task_update(id: Int!, message: String, user: Int): Task!
task_delete(id: Int!): Boolean!
user_create(username: String!, tasks: [Int!]): User!
user_update(id: Int!, username: String, tasks: [Int!]): User!
user_delete(id: Int!): Boolean!
}
ModelManager creates objects, fields, arguments, resolvers, and validators for
a model. Let’s look at what it created for the Task model:
Taskobject type with fields corresponding to each column and relationship. SeeModelManager.objectand Model Conversion.task_itemquery field that will return a row by id, or null if not found. SeeModelManager.item_field.task_listquery field that will return a result object with a list of rows and a total count. SeeModelManager.list_field.The
filterargument can apply filters to any column, including across relationships; see List Query Filters.The
sortargument can be one or more column names to sort by, including across relationships like filters. A name can begin with-to sort descending. Defaults to the primary key.The
pageandper_pagearguments apply pagination, defaulting to page 1 with 10 per page. Currently, pagination cannot be disabled and has a max of 100 per page.
task_createmutation field to create a new row, with arguments for each column and relationship. Arguments are optional if the column is nullable or has a default. SeeModelManager.create_field.task_updatemutation field to update a row by id, with arguments for each column and relationship. All column arguments are optional. SeeModelManager.update_field.task_deletemutation field to delete a row by id. SeeModelManager.delete_field.
It also provides two global queries:
searchtakes a value and searches all string columns in all models. A UI could use this to provide a global search bar. See Global Search Query.check_delete(type, id)takes a model name and row id and checks what would be affected if the row was deleted. See Preview Delete Effects Query.
Customizing the Schema¶
After generating the ModelGroup, you can modify what it generated.
This can be done before or after registering it on the schema. Just like plain
Magql, it cannot be modified after the schema is finalized by calling
Magql.to_graphql() (or execute, etc.).
ModelGroup.managers maps model names to ModelManager instances.
The manager has attributes for each object and field it generated.
For example, you could add a new field and resolver to an object.
user_manager = model_group.managers["User"]
@user_manager.object.field("greet", "String!")
def resolve_user_greet(parent: User, info, **kwargs) -> str:
return f"Hello, {parent.username}!"
You could remove a field that should not be exposed in the API.
del user_manager.object.fields["password"]
You could add a validator to an argument.
@user_manager.create_field.args["username"].validator
def validate_username(info, value: str, data):
if not value.islower():
raise magql.ValidationError("Must be lowercase.")
Anything that is possible with Magql is possible with the model group’s generated API. Be sure to review the Magql documentation.