-
Notifications
You must be signed in to change notification settings - Fork 519
/
schema.ex
660 lines (521 loc) · 16.9 KB
/
schema.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
defmodule Absinthe.Schema do
alias Absinthe.Type
alias __MODULE__
@type t :: module
@moduledoc """
Build GraphQL Schemas
## Custom Schema Manipulation (in progress)
In Absinthe 1.5 schemas are built using the same process by which queries are
executed. All the macros in this module and in `Notation` build up an intermediary tree of structs in the
`%Absinthe.Blueprint{}` namespace, which we generally call "Blueprint structs".
At the top you've got a `%Blueprint{}` struct which holds onto some schema
definitions that look a bit like this:
```
%Blueprint.Schema.SchemaDefinition{
type_definitions: [
%Blueprint.Schema.ObjectTypeDefinition{identifier: :query, ...},
%Blueprint.Schema.ObjectTypeDefinition{identifier: :mutation, ...},
%Blueprint.Schema.ObjectTypeDefinition{identifier: :user, ...},
%Blueprint.Schema.EnumTypeDefinition{identifier: :sort_order, ...},
]
}
```
You can see what your schema's blueprint looks like by calling
`__absinthe_blueprint__` on any schema or type definition module.
```
defmodule MyAppWeb.Schema do
use Absinthe.Schema
query do
end
end
> MyAppWeb.Schema.__absinthe_blueprint__
#=> %Absinthe.Blueprint{...}
```
These blueprints are manipulated by phases, which validate and ultimately
construct a schema. This pipeline of phases you can hook into like you do for
queries.
```
defmodule MyAppWeb.Schema do
use Absinthe.Schema
@pipeline_modifier MyAppWeb.CustomSchemaPhase
query do
end
end
defmodule MyAppWeb.CustomSchemaPhase do
alias Absinthe.{Phase, Pipeline, Blueprint}
# Add this module to the pipeline of phases
# to run on the schema
def pipeline(pipeline) do
Pipeline.insert_after(pipeline, Phase.Schema.TypeImports, __MODULE__)
end
# Here's the blueprint of the schema, do whatever you want with it.
def run(blueprint, _) do
{:ok, blueprint}
end
end
```
The blueprint structs are pretty complex, but if you ever want to figure out
how to construct something in blueprints you can always just create the thing
in the normal AST and then look at the output. Let's see what interfaces look
like for example:
```
defmodule Foo do
use Absinthe.Schema.Notation
interface :named do
field :name, :string
end
end
Foo.__absinthe_blueprint__ #=> ...
```
"""
defmacro __using__(opts) do
Module.register_attribute(__CALLER__.module, :pipeline_modifier,
accumulate: true,
persist: true
)
Module.register_attribute(__CALLER__.module, :prototype_schema, persist: true)
quote do
use Absinthe.Schema.Notation, unquote(opts)
import unquote(__MODULE__), only: :macros
@after_compile unquote(__MODULE__)
@before_compile unquote(__MODULE__)
@prototype_schema Absinthe.Schema.Prototype
@schema_provider Absinthe.Schema.Compiled
def __absinthe_lookup__(name) do
__absinthe_type__(name)
end
@behaviour Absinthe.Schema
@doc false
def middleware(middleware, _field, _object) do
middleware
end
@doc false
def plugins do
Absinthe.Plugin.defaults()
end
@doc false
def context(context) do
context
end
@doc false
def hydrate(_node, _ancestors) do
[]
end
defoverridable(context: 1, middleware: 3, plugins: 0, hydrate: 2)
end
end
def child_spec(schema) do
%{
id: {__MODULE__, schema},
start: {__MODULE__.Manager, :start_link, [schema]},
type: :worker
}
end
@object_type Absinthe.Blueprint.Schema.ObjectTypeDefinition
@default_query_name "RootQueryType"
@doc """
Defines a root Query object
"""
defmacro query(raw_attrs \\ [name: @default_query_name], do: block) do
record_query(__CALLER__, raw_attrs, block)
end
defp record_query(env, raw_attrs, block) do
attrs =
raw_attrs
|> Keyword.put_new(:name, @default_query_name)
Absinthe.Schema.Notation.record!(env, @object_type, :query, attrs, block)
end
@default_mutation_name "RootMutationType"
@doc """
Defines a root Mutation object
```
mutation do
field :create_user, :user do
arg :name, non_null(:string)
arg :email, non_null(:string)
resolve &MyApp.Web.BlogResolvers.create_user/2
end
end
```
"""
defmacro mutation(raw_attrs \\ [name: @default_mutation_name], do: block) do
record_mutation(__CALLER__, raw_attrs, block)
end
defp record_mutation(env, raw_attrs, block) do
attrs =
raw_attrs
|> Keyword.put_new(:name, @default_mutation_name)
Absinthe.Schema.Notation.record!(env, @object_type, :mutation, attrs, block)
end
@default_subscription_name "RootSubscriptionType"
@doc """
Defines a root Subscription object
Subscriptions in GraphQL let a client submit a document to the server that
outlines what data they want to receive in the event of particular updates.
For a full walk through of how to setup your project with subscriptions and
`Phoenix` see the `Absinthe.Phoenix` project moduledoc.
When you push a mutation, you can have selections on that mutation result
to get back data you need, IE
```
mutation {
createUser(accountId: 1, name: "bob") {
id
account { name }
}
}
```
However, what if you want to know when OTHER people create a new user, so that
your UI can update as well. This is the point of subscriptions.
```
subscription {
newUsers {
id
account { name }
}
}
```
The job of the subscription macros then is to give you the tools to connect
subscription documents with the values that will drive them. In the last example
we would get all users for all accounts, but you could imagine wanting just
`newUsers(accountId: 2)`.
In your schema you articulate the interests of a subscription via the `config`
macro:
```
subscription do
field :new_users, :user do
arg :account_id, non_null(:id)
config fn args, _info ->
{:ok, topic: args.account_id}
end
end
end
```
The topic can be any term. You can broadcast a value manually to this subscription
by doing
```
Absinthe.Subscription.publish(pubsub, user, [new_users: user.account_id])
```
It's pretty common to want to associate particular mutations as the triggers
for one or more subscriptions, so Absinthe provides some macros to help with
that too.
```
subscription do
field :new_users, :user do
arg :account_id, non_null(:id)
config fn args, _info ->
{:ok, topic: args.account_id}
end
trigger :create_user, topic: fn user ->
user.account_id
end
end
end
```
The idea with a trigger is that it takes either a single mutation `:create_user`
or a list of mutations `[:create_user, :blah_user, ...]` and a topic function.
This function returns a value that is used to lookup documents on the basis of
the topic they returned from the `config` macro.
Note that a subscription field can have `trigger` as many trigger blocks as you
need, in the event that different groups of mutations return different results
that require different topic functions.
"""
defmacro subscription(raw_attrs \\ [name: @default_subscription_name], do: block) do
record_subscription(__CALLER__, raw_attrs, block)
end
defp record_subscription(env, raw_attrs, block) do
attrs =
raw_attrs
|> Keyword.put_new(:name, @default_subscription_name)
Absinthe.Schema.Notation.record!(env, @object_type, :subscription, attrs, block)
end
defmacro __before_compile__(_) do
quote do
@doc false
def __absinthe_pipeline_modifiers__ do
[@schema_provider] ++ @pipeline_modifier
end
def __absinthe_schema_provider__ do
@schema_provider
end
def __absinthe_type__(name) do
@schema_provider.__absinthe_type__(__MODULE__, name)
end
def __absinthe_directive__(name) do
@schema_provider.__absinthe_directive__(__MODULE__, name)
end
def __absinthe_types__() do
@schema_provider.__absinthe_types__(__MODULE__)
end
def __absinthe_types__(group) do
@schema_provider.__absinthe_types__(__MODULE__, group)
end
def __absinthe_directives__() do
@schema_provider.__absinthe_directives__(__MODULE__)
end
def __absinthe_interface_implementors__() do
@schema_provider.__absinthe_interface_implementors__(__MODULE__)
end
def __absinthe_prototype_schema__() do
@prototype_schema
end
end
end
@spec apply_modifiers(Absinthe.Pipeline.t(), t) :: Absinthe.Pipeline.t()
def apply_modifiers(pipeline, schema) do
Enum.reduce(schema.__absinthe_pipeline_modifiers__, pipeline, fn
{module, function}, pipeline ->
apply(module, function, [pipeline])
module, pipeline ->
module.pipeline(pipeline)
end)
end
def __after_compile__(env, _) do
prototype_schema =
env.module
|> Module.get_attribute(:prototype_schema)
pipeline =
env.module
|> Absinthe.Pipeline.for_schema(prototype_schema: prototype_schema)
|> apply_modifiers(env.module)
env.module.__absinthe_blueprint__
|> Absinthe.Pipeline.run(pipeline)
|> case do
{:ok, _, _} ->
[]
{:error, errors, _} ->
raise Absinthe.Schema.Error, phase_errors: List.wrap(errors)
end
end
### Helpers
@doc """
Run the introspection query on a schema.
Convenience function.
"""
@spec introspect(schema :: t, opts :: Absinthe.run_opts()) :: Absinthe.run_result()
def introspect(schema, opts \\ []) do
[:code.priv_dir(:absinthe), "graphql", "introspection.graphql"]
|> Path.join()
|> File.read!()
|> Absinthe.run(schema, opts)
end
@doc """
Replace the default middleware.
## Examples
Replace the default for all fields with a string lookup instead of an atom lookup:
```
def middleware(middleware, field, object) do
new_middleware = {Absinthe.Middleware.MapGet, to_string(field.identifier)}
middleware
|> Absinthe.Schema.replace_default(new_middleware, field, object)
end
```
"""
def replace_default(middleware_list, new_middleware, %{identifier: identifer}, _object) do
Enum.map(middleware_list, fn middleware ->
case middleware do
{Absinthe.Middleware.MapGet, ^identifer} ->
new_middleware
middleware ->
middleware
end
end)
end
@doc """
Used to define the list of plugins to run before and after resolution.
Plugins are modules that implement the `Absinthe.Plugin` behaviour. These modules
have the opportunity to run callbacks before and after the resolution of the entire
document, and have access to the resolution accumulator.
Plugins must be specified by the schema, so that Absinthe can make sure they are
all given a chance to run prior to resolution.
"""
@callback plugins() :: [Absinthe.Plugin.t()]
@doc """
Used to apply middleware on all or a group of fields based on pattern matching.
It is passed the existing middleware for a field, the field itself, and the object
that the field is a part of.
## Examples
Adding a `HandleChangesetError` middleware only to mutations:
```
# if it's a field for the mutation object, add this middleware to the end
def middleware(middleware, _field, %{identifier: :mutation}) do
middleware ++ [MyAppWeb.Middleware.HandleChangesetErrors]
end
# if it's any other object keep things as is
def middleware(middleware, _field, _object), do: middleware
```
"""
@callback middleware([Absinthe.Middleware.spec(), ...], Type.Field.t(), Type.Object.t()) :: [
Absinthe.Middleware.spec(),
...
]
@doc """
Used to set some values in the context that it may need in order to run.
## Examples
Setup dataloader:
```
def context(context) do
loader =
Dataloader.new
|> Dataloader.add_source(Blog, Blog.data())
Map.put(context, :loader, loader)
end
```
"""
@callback context(map) :: map
@doc """
Used to hydrate the schema with dynamic attributes.
While this is normally used to add resolvers, etc, to schemas
defined using `import_sdl/1` and `import_sdl2`, it can also be
used in schemas defined using other macros.
The function is passed the blueprint definition node as the first
argument and its ancestors in a list (with its parent node as the
head) as its second argument.
See the `Absinthe.Phase.Schema.Hydrate` implementation of
`Absinthe.Schema.Hydrator` callbacks to see what hydration
values can be returned.
## Examples
Add a resolver for a field:
```
def hydrate(%Absinthe.Blueprint.Schema.FieldDefinition{identifier: :health}, [%Absinthe.Blueprint.Schema.ObjectTypeDefinition{identifier: :query} | _]) do
{:resolve, &__MODULE__.health/3}
end
# Resolver implementation:
def health(_, _, _), do: {:ok, "alive!"}
```
Note that the values provided must be macro-escapable; notably, anonymous functions cannot
be used.
You can, of course, omit the struct names for brevity:
```
def hydrate(%{identifier: :health}, [%{identifier: :query} | _]) do
{:resolve, &__MODULE__.health/3}
end
```
Add a description to a type:
```
def hydrate(%Absinthe.Blueprint.Schema.ObjectTypeDefinition{identifier: :user}, _) do
{:description, "A user"}
end
```
If you define `hydrate/2`, don't forget to include a fallback, e.g.:
```
def hydrate(_node, _ancestors), do: []
```
"""
@callback hydrate(
node :: Absinthe.Blueprint.Schema.t(),
ancestors :: [Absinthe.Blueprint.Schema.t()]
) :: Absinthe.Schema.Hydrator.hydration()
def lookup_directive(schema, name) do
schema.__absinthe_directive__(name)
end
def lookup_type(schema, type, options \\ [unwrap: true]) do
cond do
is_atom(type) ->
schema.__absinthe_lookup__(type)
is_binary(type) ->
schema.__absinthe_lookup__(type)
Type.wrapped?(type) ->
if Keyword.get(options, :unwrap) do
lookup_type(schema, type |> Type.unwrap())
else
type
end
true ->
type
end
end
@doc """
Get all concrete types for union, interface, or object
"""
@spec concrete_types(t, Type.t()) :: [Type.t()]
def concrete_types(schema, %Type.Union{} = type) do
Enum.map(type.types, &lookup_type(schema, &1))
end
def concrete_types(schema, %Type.Interface{} = type) do
implementors(schema, type)
end
def concrete_types(_, %Type.Object{} = type) do
[type]
end
def concrete_types(_, type) do
[type]
end
@doc """
Get all types that are used by an operation
"""
@deprecated "Use Absinthe.Schema.referenced_types/1 instead"
@spec used_types(t) :: [Type.t()]
def used_types(schema) do
referenced_types(schema)
end
@doc """
Get all types that are referenced by an operation
"""
@spec referenced_types(t) :: [Type.t()]
def referenced_types(schema) do
schema
|> Schema.types()
|> Enum.filter(&(!Type.introspection?(&1)))
end
@doc """
List all directives on a schema
"""
@spec directives(t) :: [Type.Directive.t()]
def directives(schema) do
schema.__absinthe_directives__
|> Map.keys()
|> Enum.map(&lookup_directive(schema, &1))
end
@doc """
Converts a schema to an SDL string
Per the spec, only types that are actually referenced directly or transitively from
the root query, subscription, or mutation objects are included.
## Example
Absinthe.Schema.to_sdl(MyAppWeb.Schema)
"schema {
query {...}
}"
"""
@spec to_sdl(schema :: t) :: String.t()
def to_sdl(schema) do
pipeline =
schema
|> Absinthe.Pipeline.for_schema()
|> Absinthe.Pipeline.upto({Absinthe.Phase.Schema.Validation.Result, pass: :final})
|> apply_modifiers(schema)
# we can be assertive here, since this same pipeline was already used to
# successfully compile the schema.
{:ok, bp, _} = Absinthe.Pipeline.run(schema.__absinthe_blueprint__, pipeline)
inspect(bp, pretty: true)
end
@doc """
List all implementors of an interface on a schema
"""
@spec implementors(t, Type.identifier_t() | Type.Interface.t()) :: [Type.Object.t()]
def implementors(schema, ident) when is_atom(ident) do
schema.__absinthe_interface_implementors__
|> Map.get(ident, [])
|> Enum.map(&lookup_type(schema, &1))
end
def implementors(schema, %Type.Interface{identifier: identifier}) do
implementors(schema, identifier)
end
@doc """
List all types on a schema
"""
@spec types(t) :: [Type.t()]
def types(schema) do
schema.__absinthe_types__
|> Map.keys()
|> Enum.map(&lookup_type(schema, &1))
end
@doc """
Get all introspection types
"""
@spec introspection_types(t) :: [Type.t()]
def introspection_types(schema) do
schema
|> Schema.types()
|> Enum.filter(&Type.introspection?/1)
end
end