Skip to content

Commit 9ffab5d

Browse files
authored
feat: add domain-level pubsub configuration for subscriptions (#341)
1 parent 9d80963 commit 9ffab5d

15 files changed

+525
-9
lines changed

documentation/dsls/DSL-AshGraphql.Domain.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,8 @@ Subscriptions to expose for the resource.
483483
### Examples
484484
```
485485
subscription do
486+
pubsub MyApp.PubSub
487+
486488
subscribe Post, :post_created do
487489
action_types(:create)
488490
end
@@ -493,6 +495,14 @@ end
493495

494496

495497

498+
### Options
499+
500+
| Name | Type | Default | Docs |
501+
|------|------|---------|------|
502+
| [`pubsub`](#graphql-subscriptions-pubsub){: #graphql-subscriptions-pubsub } | `module` | | The pubsub module to use for subscriptions in this domain. Resources can override this by specifying their own pubsub. |
503+
504+
505+
496506
### graphql.subscriptions.subscribe
497507
```elixir
498508
subscribe resource, name

documentation/dsls/DSL-AshGraphql.Resource.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ end
520520

521521
| Name | Type | Default | Docs |
522522
|------|------|---------|------|
523-
| [`pubsub`](#graphql-subscriptions-pubsub){: #graphql-subscriptions-pubsub .spark-required} | `module` | | The pubsub module to use for the subscription |
523+
| [`pubsub`](#graphql-subscriptions-pubsub){: #graphql-subscriptions-pubsub } | `module` | | The pubsub module to use for the subscription. If not specified, will use the domain's pubsub configuration. |
524524

525525

526526

lib/domain/domain.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,21 @@ defmodule AshGraphql.Domain do
6565
examples: [
6666
"""
6767
subscription do
68+
pubsub MyApp.PubSub
69+
6870
subscribe Post, :post_created do
6971
action_types(:create)
7072
end
7173
end
7274
"""
7375
],
76+
schema: [
77+
pubsub: [
78+
type: :module,
79+
doc:
80+
"The pubsub module to use for subscriptions in this domain. Resources can override this by specifying their own pubsub."
81+
]
82+
],
7483
entities:
7584
Enum.map(
7685
AshGraphql.Resource.subscriptions(),
@@ -152,7 +161,8 @@ defmodule AshGraphql.Domain do
152161
AshGraphql.Domain.Transformers.ValidateCompatibleNames
153162
],
154163
verifiers: [
155-
AshGraphql.Resource.Verifiers.VerifyDomainQueryMetadata
164+
AshGraphql.Resource.Verifiers.VerifyDomainQueryMetadata,
165+
AshGraphql.Domain.Verifiers.VerifySubscriptionPubsub
156166
]
157167

158168
if Code.ensure_loaded?(Igniter) do

lib/domain/info.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ defmodule AshGraphql.Domain.Info do
4646
Extension.get_entities(resource, [:graphql, :subscriptions]) || []
4747
end
4848

49+
@doc "The pubsub module configured for subscriptions in this domain"
50+
def subscription_pubsub(domain) do
51+
Extension.get_opt(domain, [:graphql, :subscriptions], :pubsub)
52+
end
53+
4954
@doc "Whether or not to render raised errors in the GraphQL response"
5055
def show_raised_errors?(domain) do
5156
Extension.get_opt(domain, [:graphql], :show_raised_errors?, false, true)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
defmodule AshGraphql.Domain.Verifiers.VerifySubscriptionPubsub do
2+
@moduledoc """
3+
Verifies that pubsub is properly configured for subscriptions at the domain level.
4+
5+
This verifier ensures that:
6+
- If a domain has subscriptions, it either has pubsub configured or all its resources with subscriptions have pubsub configured
7+
- Resources with subscriptions have pubsub available either at the resource level or domain level
8+
"""
9+
10+
use Spark.Dsl.Verifier
11+
12+
def verify(dsl) do
13+
domain_subscriptions = AshGraphql.Domain.Info.subscriptions(dsl)
14+
domain_pubsub = AshGraphql.Domain.Info.subscription_pubsub(dsl)
15+
16+
if is_nil(domain_pubsub) do
17+
already_checked =
18+
if domain_subscriptions != [] do
19+
domain_subscriptions
20+
|> Enum.map(fn subscription ->
21+
resource = subscription.resource
22+
23+
Code.ensure_loaded!(resource)
24+
25+
resource_pubsub = AshGraphql.Resource.Info.subscription_pubsub(resource)
26+
27+
unless resource_pubsub do
28+
raise Spark.Error.DslError,
29+
module: Spark.Dsl.Transformer.get_persisted(resource.spark_dsl_config(), :module),
30+
message:
31+
"Domain subscription for #{inspect(resource)} requires pubsub to be configured either at the domain level or on the resource #{inspect(resource)} itself.",
32+
path: [:graphql, :subscriptions, subscription.name]
33+
end
34+
35+
resource
36+
end)
37+
else
38+
[]
39+
end
40+
41+
dsl
42+
|> Ash.Domain.Info.resources()
43+
|> Kernel.--(already_checked)
44+
|> Enum.each(fn resource ->
45+
Code.ensure_loaded!(resource)
46+
47+
resource_subscriptions = AshGraphql.Resource.Info.subscriptions(resource, dsl)
48+
49+
if resource_subscriptions != [] do
50+
resource_pubsub = AshGraphql.Resource.Info.subscription_pubsub(resource)
51+
52+
unless resource_pubsub do
53+
raise Spark.Error.DslError,
54+
module: Spark.Dsl.Transformer.get_persisted(resource.spark_dsl_config(), :module),
55+
message:
56+
"Resource #{inspect(resource)} has subscriptions but no pubsub module configured. A pubsub module must be specified either at the resource level (in the subscriptions section) or at the domain level (in the domain's graphql subscriptions section).",
57+
path: [:graphql, :subscriptions, :pubsub]
58+
end
59+
end
60+
end)
61+
end
62+
63+
:ok
64+
end
65+
end

lib/resource/info.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,16 @@ defmodule AshGraphql.Resource.Info do
5252
end
5353

5454
@doc "The pubsub module used for subscriptions"
55-
def subscription_pubsub(resource) do
56-
Extension.get_opt(resource, [:graphql, :subscriptions], :pubsub)
55+
def subscription_pubsub(resource, domain_or_domains \\ []) do
56+
case Extension.get_opt(resource, [:graphql, :subscriptions], :pubsub) do
57+
nil ->
58+
domain_or_domains
59+
|> List.wrap()
60+
|> Enum.find_value(&AshGraphql.Domain.Info.subscription_pubsub/1)
61+
62+
pubsub_module ->
63+
pubsub_module
64+
end
5765
end
5866

5967
@doc "Wether or not to encode the primary key as a single `id` field when reading and getting"

lib/resource/resource.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,8 @@ defmodule AshGraphql.Resource do
308308
schema: [
309309
pubsub: [
310310
type: :module,
311-
required: true,
312-
doc: "The pubsub module to use for the subscription"
311+
doc:
312+
"The pubsub module to use for the subscription. If not specified, will use the domain's pubsub configuration."
313313
]
314314
],
315315
describe: """

lib/subscription/notifier.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule AshGraphql.Subscription.Notifier do
99

1010
@impl Ash.Notifier
1111
def notify(%Ash.Notifier.Notification{} = notification) do
12-
pub_sub = Info.subscription_pubsub(notification.resource)
12+
pub_sub = Info.subscription_pubsub(notification.resource, notification.domain)
1313

1414
for subscription <-
1515
AshGraphql.Resource.Info.subscriptions(notification.resource, notification.domain) do
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
defmodule AshGraphql.DomainPubsubValidationTest do
2+
use ExUnit.Case, async: false
3+
4+
describe "domain pubsub validation during compilation" do
5+
test "raises error when resource has subscriptions but no pubsub and domain has no pubsub" do
6+
assert_raise Spark.Error.DslError, fn ->
7+
defmodule TestResourceWithoutPubsub do
8+
use Ash.Resource,
9+
domain: TestDomainWithoutPubsub,
10+
extensions: [AshGraphql.Resource]
11+
12+
graphql do
13+
type :test_resource_without_pubsub
14+
15+
subscriptions do
16+
subscribe(:test_subscription) do
17+
action_types([:create])
18+
end
19+
end
20+
end
21+
22+
actions do
23+
default_accept(:*)
24+
defaults([:create])
25+
end
26+
27+
attributes do
28+
uuid_primary_key(:id)
29+
end
30+
end
31+
32+
defmodule TestDomainWithoutPubsub do
33+
use Ash.Domain, extensions: [AshGraphql.Domain]
34+
35+
graphql do
36+
subscriptions do
37+
end
38+
end
39+
40+
resources do
41+
resource(TestResourceWithoutPubsub)
42+
end
43+
end
44+
end
45+
end
46+
47+
test "raises error when domain has subscriptions but no pubsub and resource has no pubsub" do
48+
assert_raise Spark.Error.DslError, fn ->
49+
defmodule TestResourceWithoutPubsub2 do
50+
use Ash.Resource,
51+
domain: TestDomainWithoutPubsub2,
52+
extensions: [AshGraphql.Resource]
53+
54+
graphql do
55+
type :test_resource_without_pubsub2
56+
57+
subscriptions do
58+
subscribe(:test_subscription) do
59+
action_types([:create])
60+
end
61+
end
62+
end
63+
64+
actions do
65+
default_accept(:*)
66+
defaults([:create])
67+
end
68+
69+
attributes do
70+
uuid_primary_key(:id)
71+
end
72+
end
73+
74+
defmodule TestDomainWithoutPubsub2 do
75+
use Ash.Domain, extensions: [AshGraphql.Domain]
76+
77+
graphql do
78+
subscriptions do
79+
subscribe TestResourceWithoutPubsub2, :test_subscription do
80+
action_types([:create])
81+
end
82+
end
83+
end
84+
85+
resources do
86+
resource(TestResourceWithoutPubsub2)
87+
end
88+
end
89+
end
90+
end
91+
92+
test "succeeds when domain has pubsub and resource has no pubsub" do
93+
defmodule TestResourceWithoutPubsub3 do
94+
use Ash.Resource, domain: TestDomainWithPubsub3, extensions: [AshGraphql.Resource]
95+
96+
graphql do
97+
type :test_resource_without_pubsub3
98+
99+
subscriptions do
100+
subscribe(:test_subscription) do
101+
action_types([:create])
102+
end
103+
end
104+
end
105+
106+
actions do
107+
default_accept(:*)
108+
defaults([:create])
109+
end
110+
111+
attributes do
112+
uuid_primary_key(:id)
113+
end
114+
end
115+
116+
defmodule TestDomainWithPubsub3 do
117+
use Ash.Domain, extensions: [AshGraphql.Domain]
118+
119+
graphql do
120+
subscriptions do
121+
pubsub AshGraphql.Test.PubSub
122+
end
123+
end
124+
125+
resources do
126+
resource(TestResourceWithoutPubsub3)
127+
end
128+
end
129+
130+
assert TestDomainWithPubsub3
131+
assert TestResourceWithoutPubsub3
132+
end
133+
134+
test "succeeds when resource has pubsub and domain has no pubsub" do
135+
defmodule TestResourceWithPubsub4 do
136+
use Ash.Resource, domain: TestDomainWithoutPubsub4, extensions: [AshGraphql.Resource]
137+
138+
graphql do
139+
type :test_resource_with_pubsub4
140+
141+
subscriptions do
142+
pubsub AshGraphql.Test.PubSub
143+
144+
subscribe(:test_subscription) do
145+
action_types([:create])
146+
end
147+
end
148+
end
149+
150+
actions do
151+
default_accept(:*)
152+
defaults([:create])
153+
end
154+
155+
attributes do
156+
uuid_primary_key(:id)
157+
end
158+
end
159+
160+
defmodule TestDomainWithoutPubsub4 do
161+
use Ash.Domain, extensions: [AshGraphql.Domain]
162+
163+
graphql do
164+
subscriptions do
165+
end
166+
end
167+
168+
resources do
169+
resource(TestResourceWithPubsub4)
170+
end
171+
end
172+
173+
assert TestDomainWithoutPubsub4
174+
assert TestResourceWithPubsub4
175+
end
176+
end
177+
end

0 commit comments

Comments
 (0)