Skip to content

Commit

Permalink
feat: subscription dsl (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
barnabasJ authored Sep 27, 2024
1 parent 92ace49 commit 4c377e5
Show file tree
Hide file tree
Showing 29 changed files with 1,512 additions and 22 deletions.
7 changes: 7 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ spark_locals_without_parens = [
action: 2,
action: 3,
action: 4,
action_types: 1,
actions: 1,
actor: 1,
allow_nil?: 1,
argument_input_types: 1,
argument_names: 1,
Expand Down Expand Up @@ -47,6 +50,7 @@ spark_locals_without_parens = [
paginate_relationship_with: 1,
paginate_with: 1,
primary_key_delimiter: 1,
pubsub: 1,
read_action: 1,
read_one: 2,
read_one: 3,
Expand All @@ -58,6 +62,9 @@ spark_locals_without_parens = [
show_fields: 1,
show_metadata: 1,
show_raised_errors?: 1,
subscribe: 1,
subscribe: 2,
subscribe: 3,
tracer: 1,
type: 1,
type_name: 1,
Expand Down
5 changes: 5 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ config :ash, :validate_domain_config_inclusion?, false

config :logger, level: :warning

config :ash, :pub_sub, debug?: true
config :logger, level: :info

config :ash_graphql, :subscriptions, true

if Mix.env() == :dev do
config :git_ops,
mix_project: AshGraphql.MixProject,
Expand Down
74 changes: 71 additions & 3 deletions documentation/dsls/DSL:-AshGraphql.Domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Domain level configuration for GraphQL
* update
* destroy
* action
* [subscriptions](#graphql-subscriptions)
* subscribe


### Examples
Expand Down Expand Up @@ -269,9 +271,9 @@ Mutations (create/update/destroy actions) to expose for the resource.
### Examples
```
mutations do
create :create_post, :create
update :update_post, :update
destroy :destroy_post, :destroy
create Post, :create_post, :create
update Post, :update_post, :update
destroy Post, :destroy_post, :destroy
end
```
Expand Down Expand Up @@ -445,6 +447,72 @@ action :check_status, :check_status
Target: `AshGraphql.Resource.Action`


## graphql.subscriptions
Subscriptions to expose for the resource.


### Nested DSLs
* [subscribe](#graphql-subscriptions-subscribe)


### Examples
```
subscription do
subscribe Post, :post_created do
action_types(:create)
end
end
```




## graphql.subscriptions.subscribe
```elixir
subscribe resource, name
```


A subscription to listen for changes on the resource



### Examples
```
subscribe :post_created do
action_types(:create)
end
```



### Arguments

| Name | Type | Default | Docs |
|------|------|---------|------|
| [`resource`](#graphql-subscriptions-subscribe-resource){: #graphql-subscriptions-subscribe-resource } | `module` | | The resource that the action is defined on |
| [`name`](#graphql-subscriptions-subscribe-name){: #graphql-subscriptions-subscribe-name } | `atom` | | The name to use for the subscription. |
### Options

| Name | Type | Default | Docs |
|------|------|---------|------|
| [`actor`](#graphql-subscriptions-subscribe-actor){: #graphql-subscriptions-subscribe-actor } | `(any -> any) \| module` | | The actor to use for authorization. |
| [`actions`](#graphql-subscriptions-subscribe-actions){: #graphql-subscriptions-subscribe-actions } | `list(atom) \| atom` | | The create/update/destroy actions the subsciption should listen to. |
| [`action_types`](#graphql-subscriptions-subscribe-action_types){: #graphql-subscriptions-subscribe-action_types } | `list(atom) \| atom` | | The type of actions the subsciption should listen to. |
| [`read_action`](#graphql-subscriptions-subscribe-read_action){: #graphql-subscriptions-subscribe-read_action } | `atom` | | The read action to use for reading data |
| [`hide_inputs`](#graphql-subscriptions-subscribe-hide_inputs){: #graphql-subscriptions-subscribe-hide_inputs } | `list(atom)` | `[]` | A list of inputs to hide from the subscription, usable if the read action has arguments. |





### Introspection

Target: `AshGraphql.Resource.Subscription`





Expand Down
76 changes: 76 additions & 0 deletions documentation/dsls/DSL:-AshGraphql.Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Configuration for a given resource in graphql
* update
* destroy
* action
* [subscriptions](#graphql-subscriptions)
* subscribe
* [managed_relationships](#graphql-managed_relationships)
* managed_relationship

Expand Down Expand Up @@ -464,6 +466,80 @@ action :check_status, :check_status
Target: `AshGraphql.Resource.Action`


## graphql.subscriptions
Subscriptions (notifications) to expose for the resource.


### Nested DSLs
* [subscribe](#graphql-subscriptions-subscribe)


### Examples
```
subscriptions do
subscribe :bucket_created do
actions :create
read_action :read
end
end
```




### Options

| Name | Type | Default | Docs |
|------|------|---------|------|
| [`pubsub`](#graphql-subscriptions-pubsub){: #graphql-subscriptions-pubsub .spark-required} | `module` | | The pubsub module to use for the subscription |



## graphql.subscriptions.subscribe
```elixir
subscribe name
```


A subscription to listen for changes on the resource



### Examples
```
subscribe :post_created do
action_types(:create)
end
```



### Arguments

| Name | Type | Default | Docs |
|------|------|---------|------|
| [`name`](#graphql-subscriptions-subscribe-name){: #graphql-subscriptions-subscribe-name } | `atom` | | The name to use for the subscription. |
### Options

| Name | Type | Default | Docs |
|------|------|---------|------|
| [`actor`](#graphql-subscriptions-subscribe-actor){: #graphql-subscriptions-subscribe-actor } | `(any -> any) \| module` | | The actor to use for authorization. |
| [`actions`](#graphql-subscriptions-subscribe-actions){: #graphql-subscriptions-subscribe-actions } | `list(atom) \| atom` | | The create/update/destroy actions the subsciption should listen to. |
| [`action_types`](#graphql-subscriptions-subscribe-action_types){: #graphql-subscriptions-subscribe-action_types } | `list(atom) \| atom` | | The type of actions the subsciption should listen to. |
| [`read_action`](#graphql-subscriptions-subscribe-read_action){: #graphql-subscriptions-subscribe-read_action } | `atom` | | The read action to use for reading data |
| [`hide_inputs`](#graphql-subscriptions-subscribe-hide_inputs){: #graphql-subscriptions-subscribe-hide_inputs } | `list(atom)` | `[]` | A list of inputs to hide from the subscription, usable if the read action has arguments. |





### Introspection

Target: `AshGraphql.Resource.Subscription`


## graphql.managed_relationships
Generates input objects for `manage_relationship` arguments on resource actions.

Expand Down
76 changes: 75 additions & 1 deletion documentation/topics/use-subscriptions-with-graphql.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Using Subscriptions

The AshGraphql DSL does not currently support subscriptions. However, you can do this with Absinthe direclty, and use `AshGraphql.Subscription.query_for_subscription/3`. Here is an example of how you could do this for a subscription for a single record. This example could be extended to support lists of records as well.
You can do this with Absinthe directly, and use `AshGraphql.Subscription.query_for_subscription/3`. Here is an example of how you could do this for a subscription for a single record. This example could be extended to support lists of records as well.

```elixir
# in your absinthe schema file
Expand All @@ -27,3 +27,77 @@ subscription do
end
end
```

## Subscription DSL (beta)

The subscription DSL is currently in beta and before using it you have to enable them in your config.

```elixir
config :ash_graphql, :policies, show_policy_breakdowns?: true
```

First you'll need to do some setup, follow the the [setup guide](https://hexdocs.pm/absinthe/subscriptions.html#absinthe-phoenix-setup)
in the absinthe docs, but instead of using `Absinthe.Pheonix.Endpoint` use `AshGraphql.Subscription.Endpoint`.

Afterwards add an empty subscription block to your schema module.

```elixir
defmodule MyAppWeb.Schema do
...

subscription do
end
end
```

Now you can define subscriptions on your resource or domain

```elixir
defmodule MyApp.Resource do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
extensions: [AshGraphql.Resource]

graphql do
subscriptions do
subscribe :resource_created do
action_types :create
end
end
end
end
```

For further Details checkout the DSL docs for [resource](/documentation/dsls/DSL:-AshGraphql.Resource.md#graphql-subscriptions) and [domain](/documentation/dsls/DSL:-AshGraphql.Domain.md#graphql-subscriptions)

### Deduplication

By default, Absinthe will deduplicate subscriptions based on the `context_id`.
We use the some of the context like actor and tenant to create a `context_id` for you.

If you want to customize the deduplication you can do so by adding a actor function to your subscription.
This function will be called with the actor that subscribes and you can return a more generic actor, this
way you can have one actor for multiple users, which will lead to less resolver executions.

```elixir
defmodule MyApp.Resource do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
extensions: [AshGraphql.Resource]

graphql do
subscriptions do
subscribe :resource_created do
action_types :create
actor fn actor ->
if check_actor(actor) do
%{id: "your generic actor", ...}
else
actor
end
end
end
end
end
end
```
19 changes: 16 additions & 3 deletions lib/ash_graphql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ defmodule AshGraphql do
@dialyzer {:nowarn_function, {:run, 2}}
def run(blueprint, _opts) do
domain = unquote(domain)

action_middleware = unquote(action_middleware)

all_domains = unquote(Enum.map(domains, &elem(&1, 0)))
Expand Down Expand Up @@ -204,6 +205,18 @@ defmodule AshGraphql do
Absinthe.Blueprint.add_field(blueprint, "RootMutationType", mutation)
end)

blueprint_with_subscriptions =
domain
|> AshGraphql.Domain.subscriptions(
all_domains,
unquote(resources),
action_middleware,
unquote(schema)
)
|> Enum.reduce(blueprint_with_mutations, fn subscription, blueprint ->
Absinthe.Blueprint.add_field(blueprint, "RootSubscriptionType", subscription)
end)

managed_relationship_types =
AshGraphql.Resource.managed_relationship_definitions(
Process.get(:managed_relationship_requirements, []),
Expand All @@ -212,7 +225,7 @@ defmodule AshGraphql do
|> Enum.uniq_by(& &1.identifier)
|> Enum.reject(fn type ->
existing_types =
case blueprint_with_mutations do
case blueprint_with_subscriptions do
%{schema_definitions: [%{type_definitions: type_definitions}]} ->
type_definitions

Expand Down Expand Up @@ -293,7 +306,7 @@ defmodule AshGraphql do
end

new_defs =
List.update_at(blueprint_with_mutations.schema_definitions, 0, fn schema_def ->
List.update_at(blueprint_with_subscriptions.schema_definitions, 0, fn schema_def ->
%{
schema_def
| type_definitions:
Expand All @@ -302,7 +315,7 @@ defmodule AshGraphql do
}
end)

{:ok, %{blueprint_with_mutations | schema_definitions: new_defs}}
{:ok, %{blueprint_with_subscriptions | schema_definitions: new_defs}}
end
end

Expand Down
Loading

0 comments on commit 4c377e5

Please sign in to comment.