Current section
Files
Jump to
Current section
Files
lib/crudry_resolver.ex
defmodule Crudry.Resolver do
@moduledoc """
Generates CRUD functions to DRY Absinthe Resolvers.
Requires a previously generated Context.
## Usage
To generate CRUD functions for a given schema resolver, simply do
defmodule MyApp.MyResolver do
alias MyApp.Repo
alias MyApp.MyContext
alias MyApp.MySchema
require Crudry.Resolver
Crudry.Resolver.generate_functions MyContext, MySchema
end
And the resolver will have all these functions available:
defmodule MyApp.MyResolver do
alias MyApp.Repo
alias MyApp.MyContext
alias MyApp.MySchema
require Crudry.Resolver
def get_my_schema(%{id: id}, _info) do
id
|> MyContext.get_my_schema()
|> nil_to_error("my_schema", fn record -> {:ok, record} end)
end
def list_my_schemas(_args, _info) do
{:ok, MyContext.list_my_schemas([])}
end
def create_my_schema(%{params: params}, _info) do
MyContext.create_my_schema(params)
end
def update_my_schema(%{id: id, params: params}, _info) do
id
|> MyContext.get_my_schema()
|> nil_to_error("my_schema", fn record -> MyContext.update_my_schema(record, params) end)
end
def delete_my_schema(%{id: id}, _info) do
id
|> MyContext.get_my_schema()
|> nil_to_error("my_schema", fn record -> MyContext.delete_my_schema(record) end)
end
# If `result` is `nil`, return an error. Otherwise, apply `func` to the `result`.
def nil_to_error(result, name, func) do
case result do
nil -> {:error, %{message: "not found", schema: name}}
%{} = record -> func.(record)
end
end
def add_info_to_custom_query(custom_query, info) do
fn initial_query -> custom_query.(initial_query, info) end
end
end
"""
@all_functions ~w(get list create update delete)a
# Always generate helper functions since they are used in the other generated functions
@helper_functions ~w(nil_to_error add_info_to_custom_query)a
@doc """
Sets default options for the resolver.
## Options
* `:only` - list of functions to be generated. If not empty, functions not
specified in this list are not generated. Defaults to `[]`.
* `:except` - list of functions to not be generated. If not empty, only functions not specified
in this list will be generated. Defaults to `[]`.
* `list_opts` - options for the `list` function. See available options in `Crudry.Query.list/2`. Defaults to `[]`.
* `create_resolver` - custom `create` resolver function with arity 4. Receives the following arguments: [Context, schema_name, args, info]. Defaults to `nil`.
* `not_found_message` - custom message for the `nil_to_error` function. Defaults to `"not found"`.
* `primary_key` - custom primary key argument to use in get, update and delete resolvers. Defaults to `:id`.
Note: in `list_opts`, `custom_query` will receive absinthe's info as the second argument and, therefore, must have arity 2. See example in `generate_functions/3`.
The accepted values for `:only` and `:except` are: `#{inspect(@all_functions)}`.
## Examples
iex> Crudry.Resolver.default only: [:create, :list]
:ok
iex> Crudry.Resolver.default except: [:get!, :list, :delete]
:ok
iex> Crudry.Resolver.default list_opts: [order_by: :id]
:ok
iex> Crudry.Resolver.default list_opts: [custom_query: &scope_list/2]
:ok
"""
defmacro default(opts) do
Module.put_attribute(__CALLER__.module, :only, opts[:only])
Module.put_attribute(__CALLER__.module, :except, opts[:except])
Module.put_attribute(__CALLER__.module, :list_opts, opts[:list_opts])
Module.put_attribute(__CALLER__.module, :create_resolver, opts[:create_resolver])
Module.put_attribute(__CALLER__.module, :update_resolver, opts[:update_resolver])
Module.put_attribute(__CALLER__.module, :delete_resolver, opts[:delete_resolver])
Module.put_attribute(__CALLER__.module, :not_found_message, opts[:not_found_message])
Module.put_attribute(__CALLER__.module, :primary_key, opts[:primary_key])
end
@doc """
Generates CRUD functions for the `schema_module` resolver.
Custom options can be given. To see the available options, refer to the documenation of `Crudry.Resolver.default/1`, noting that the `not_found_message` must only be configured using `default/1`.
## Examples
### Overriding functions
To define a custom function, add it to `except` and define your own. In the following example, the schema is updated with its association.
defmodule MyApp.Resolver do
alias MyApp.Repo
alias MyApp.MyContext
alias MyApp.MySchema
require Crudry.Resolver
Crudry.Resolver.generate_functions MyContext, MySchema, except: [:update]
def update_my_schema(%{id: id, params: params}, _info) do
id
|> MyContext.get_my_schema()
|> nil_to_error("My Schema", fn record -> MyContext.update_my_schema_with_assocs(record, params, [:assoc]) end)
end
end
By using the `nil_to_error` function, we DRY the nil checking and also ensure the error message is the same as the other auto-generated functions.
### Custom create resolver
It's possible to define a custom create function, useful when all functions in a resolver change the data in the same way before creating:
defmodule MyApp.Resolver do
alias MyApp.Repo
alias MyApp.MyContext
alias MyApp.MySchema
require Crudry.Resolver
Crudry.Resolver.default create_resolver: &create_resolver/4)
Crudry.Resolver.generate_functions MyContext, MySchema
Crudry.Resolver.generate_functions MyContext, OtherSchema
def create_resolver(context, schema_name, args, %{context: %{current_user: %{company_id: company_id}}}) do
apply(context, :"create_\#{schema_name}", [Map.put(args.params, :company_id, company_id)])
end
end
Now when creating `MySchema` and `OtherSchema`, the custom function will put the current user's `company_id` in the params.
### Custom query
It's also possible to use a custom query that has access to absinthe's `info`:
defmodule MyApp.Resolver do
alias MyApp.Repo
alias MyApp.MyContext
alias MyApp.MySchema
require Crudry.Resolver
def scope_list(MySchema, %{context: %{current_user: current_user}} = _info) do
where(MySchema, [p], p.user_id == ^current_user.id)
end
Crudry.Resolver.generate_functions MyContext, MySchema, list_opts: [custom_query: &scope_list/2]
end
With this, the list function will effectively become:
def list_my_schemas(_args, info) do
{:ok, MyContext.list_my_schemas(custom_query: add_info_to_custom_query(scope_list, info))}
end
See `Crudry.Query.list/2` for further information on the `custom_query` option.
"""
defmacro generate_functions(context, schema_module, opts \\ []) do
if Keyword.has_key?(opts, :not_found_message), do: raise "not_found_message can only be configured using default/1"
opts = Keyword.merge(load_default(__CALLER__.module), opts)
name = Helper.get_underscored_name(schema_module)
pluralized_name = Helper.get_pluralized_name(schema_module, __CALLER__)
_ = String.to_atom(name)
for func <- Helper.get_functions_to_be_generated(__CALLER__.module, @all_functions, @helper_functions, opts) do
ResolverFunctionsGenerator.generate_function(func, name, pluralized_name, context, opts)
end
end
# Load user-defined defaults or fall back to the library's default.
defp load_default(module) do
only = Module.get_attribute(module, :only)
except = Module.get_attribute(module, :except)
list_opts = Module.get_attribute(module, :list_opts)
create_resolver = Module.get_attribute(module, :create_resolver)
update_resolver = Module.get_attribute(module, :update_resolver)
delete_resolver = Module.get_attribute(module, :delete_resolver)
not_found_message = Module.get_attribute(module, :not_found_message)
primary_key = Module.get_attribute(module, :primary_key)
[
only: only || [],
except: except || [],
list_opts: list_opts || [],
create_resolver: create_resolver || nil,
update_resolver: update_resolver || nil,
delete_resolver: delete_resolver || nil,
not_found_message: not_found_message || "not found",
primary_key: primary_key || :id
]
end
end