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