zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

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