You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Relevant to edgedb/edgedb#4272 , having decorators for handling mutation and action triggers would be very beneficial for developers writing client-facing solutions with EdgeDB.
These are only my thoughts and ideas on introducing a more DX-friendly approach to writing trigger handlers for the client library. I'm more than happy to receive criticism or feedback towards this! 👋🏼
Motive
Decorators are an easy way for developers to overly simplify tasks, specifically handling events. In Discord's jargon we could consider these as "event handlers," although for EdgeDB this is much different. The premise is that a decorator can be used to "hook" a typing.Callable signature, (a function) to be called when a trigger has occurred within EdgeDB.
Design
There can be numerous types of triggers. So far, I am only aware of action and mutation triggers. Because we want the developer to have the ability to differentiate between the two, we can introduce a TriggerType enumerable to better represent these and make it clear for the client-facing code what trigger you want.
importenumclassTriggerType(enum.IntEnum):
ACTION=1MUTATION=2
... # future types of triggers you wish to associate.
The trigger itself has to be registered as a decorator. In my example, I only have a mockup for an asynchronous/non-blocking solution which takes in typing.Coroutine. As this client library allows blocking calls as well, I don't have any ideas for how I'd do it that way.
deftrigger(
self,
coro: typing.Coroutine,
type: typing.Union[int, TriggerType]
fields: typing.Union[str, typing.List[str]]
) ->typing.Callable[..., typing.Any]:
defdecor(coro: typing.Coroutine):
... # black magic and sorcery is done here. cast your spells!returndecor
Usage
The usage of these decorators would be very simple: you give in one argument as a single field name, or a list of field names you want to trigger the callable off of.
First, we would need to establish our client and make a query.
importedgedbimportlogginglogger=logging.getLogger(__file__)
client=edgedb.create_client()
query=client.query("""CREATE TYPE test { CREATE REQUIRED PROPERTY foo -> std::str;};""")
We can then use query here to associate a trigger via. a decorator.
# Note that the kwargs shown are not required, it just helps clarify what is being inputted.@query.trigger(type=edgedb.TriggerType.ACTION, fields="field_name")
The problem with this proposal is that query would need to have a manager class like QueryManager in order to use decorators like this.
One way we could make this work is by making the __repr__ magic of the manager return the result of our query call. (non-breaking)
The other solution, which would be breaking, would be to alienate its return as query.content.
After that, you can place underneath an asynchronous task or coroutine.
Here, you notice that we require 1 positional argument in the coroutine, ctx. This represents the context of our query, which we can provide to the developer if they so benefit from this. This may be particularly useful in these situations:
Numerous types or groups share the same field name, and want to create a general trigger for them.
The developer wants to log or trace back supplied arguments for a query.
Class-bound triggers
Developers may also want to run their triggers inside of classes for organisation reasons. This should be possible so as long as the client is being supplied somehow. Note that with classes, the drawback is making use of the __call__ magic. The only decent method I know for this is by subclassing from another class that already inherits a client.
We will also have to have a way to wrap the QueryManager's decorator.
classMyClass(edgedb.TriggerClass):
def__init__(self, client):
super().__init__(client)
@edgedb.class_trigger(type=edgedb.TriggerType.MUTATION, fields=["foo", "bar"])asyncdefclass_callback(self, ctx: edgedb.QueryContext) ->None:
logger.debug("Numerous fields triggered me within the class.")
Caveats
With the introduction of decorators, there are admittedly a lot of things that would have to change for this proposal to work. These can be summed up as:
Having client.query() return a QueryManager which essentially acts as a class for holding decorators and any necessary information.
"Wrapping" the decorator of the query manager class to work inside of classes, usually through functools.wraps().
Potential breaking changes would be induced by how QueryManager is structured to contain data.
Additionally, this proposal implicitly brings about some general limitations to how you can create a handler for triggers:
A trigger can only be set on an executed query() call, meaning that you can't write any "listeners" or watchers. I would love to do this, but I believe it would cause confusion, and having it associated to a made query makes it explicitly clear on what's being triggered on what condition.
Triggers only assume that something happens to a field:
An ACTION trigger type would presume a declaration has been made, such as creating a new property.
MUTATION assumes that data that has already existed has been modified in some form or variation, such as DROP.
The text was updated successfully, but these errors were encountered:
I think we should instead adopt the JS event receiver pattern or straight async for event in client.listen_for(...) syntax.
That said, the way we expose this in Python is a relatively minor design aspect, we first need to design the EdgeQL/ESDL parts as they will affect the design of everything else.
Lastly, I don't expect us to be able to listen on triggers. Triggers will be able to emit events (a separate mechanism), and we'll be listening for those.
Introduction
Relevant to edgedb/edgedb#4272 , having decorators for handling mutation and action triggers would be very beneficial for developers writing client-facing solutions with EdgeDB.
These are only my thoughts and ideas on introducing a more DX-friendly approach to writing trigger handlers for the client library. I'm more than happy to receive criticism or feedback towards this! 👋🏼
Motive
Decorators are an easy way for developers to overly simplify tasks, specifically handling events. In Discord's jargon we could consider these as "event handlers," although for EdgeDB this is much different. The premise is that a decorator can be used to "hook" a
typing.Callable
signature, (a function) to be called when a trigger has occurred within EdgeDB.Design
There can be numerous types of triggers. So far, I am only aware of action and mutation triggers. Because we want the developer to have the ability to differentiate between the two, we can introduce a
TriggerType
enumerable to better represent these and make it clear for the client-facing code what trigger you want.The trigger itself has to be registered as a decorator. In my example, I only have a mockup for an asynchronous/non-blocking solution which takes in
typing.Coroutine
. As this client library allows blocking calls as well, I don't have any ideas for how I'd do it that way.Usage
The usage of these decorators would be very simple: you give in one argument as a single field name, or a list of field names you want to trigger the callable off of.
First, we would need to establish our client and make a query.
We can then use
query
here to associate a trigger via. a decorator.The problem with this proposal is that
query
would need to have a manager class likeQueryManager
in order to use decorators like this.One way we could make this work is by making the
__repr__
magic of the manager return the result of our query call. (non-breaking)The other solution, which would be breaking, would be to alienate its return as
query.content
.After that, you can place underneath an asynchronous task or coroutine.
Here, you notice that we require 1 positional argument in the coroutine,
ctx
. This represents the context of our query, which we can provide to the developer if they so benefit from this. This may be particularly useful in these situations:Class-bound triggers
Developers may also want to run their triggers inside of classes for organisation reasons. This should be possible so as long as the client is being supplied somehow. Note that with classes, the drawback is making use of the
__call__
magic. The only decent method I know for this is by subclassing from another class that already inherits a client.We will also have to have a way to wrap the
QueryManager
's decorator.Caveats
With the introduction of decorators, there are admittedly a lot of things that would have to change for this proposal to work. These can be summed up as:
client.query()
return aQueryManager
which essentially acts as a class for holding decorators and any necessary information.functools.wraps()
.QueryManager
is structured to contain data.Additionally, this proposal implicitly brings about some general limitations to how you can create a handler for triggers:
query()
call, meaning that you can't write any "listeners" or watchers. I would love to do this, but I believe it would cause confusion, and having it associated to a made query makes it explicitly clear on what's being triggered on what condition.ACTION
trigger type would presume a declaration has been made, such as creating a new property.MUTATION
assumes that data that has already existed has been modified in some form or variation, such asDROP
.The text was updated successfully, but these errors were encountered: