View Source Indexed.Managed (Indexed v0.3.4)

Assists a GenServer in managing in-memory caches.

By annotating the entities to be managed, manage/5 can handle updating the cache for the given record and its associated records. (If associations are not preloaded, they will be automatically fetched.) In addition, entites with :subscribe and :unsubscribe functions defined will be automatically subscribed to and unusbscribed from as the first reference appears and the last one is dropped.

example

Example

This module owns and is responsible for affecting changes on the Car with id 1. It subscribes to updates to Person records as they may be updated elsewhere.

defmodule MyApp.CarManager do
  use GenServer
  use Indexed.Managed, repo: MyApp.Repo
  alias MyApp.{Car, Person, Repo}

  managed :cars, Car, children: [:passengers], manage_path: :passengers

  managed :people, Person,
    subscribe: &MyApp.subscribe_to_person/1,
    unsubscribe: &MyApp.unsubscribe_from_person/1

  @impl GenServer
  def init(_), do: {:ok, warm(:cars, Repo.get(Car, 1))}

  @impl GenServer
  def handle_call(:get, _from state) do
    {:reply, get(state, :cars, 1)}
  end

  def handle_call({:update, params}, _from, state) do
    case state |> get(:cars, 1) |> Car.changeset(params) |> Repo.update() do
      {:ok, new_car} = ok -> {:reply, ok, manage(state, :cars, :update, new_car)}
      {:error, _} = err -> {:reply, err, state}
    end
  end

  @impl GenServer
  def handle_info({MyApp, [:person, :update], person}, state) do
    {:noreply, manage(state, :people, :update, person)}
  end
end

managed-macro

Managed Macro

For each managed entity, the name (eg. :cars) and module (eg. MyApp.Car) must be specified. If needed, a keyword list of options should follow.

  • children : Keyword list with association fields as keys and assoc_spec/0s as vals. This is used when recursing in manage/5 as well as when resolving. If an undeclared association is resolved, Repo.get/2 will be used as a fallback. Will automatically include fields given in manage_path.
  • fields : List of fields which should be sortable (ascending and descending). See Indexed.Entity.
  • id_key : Specifies how to find the id for a record. It can be an atom field name to access, a function, or a tuple in the form {module, function_name}. In the latter two cases, the record will be passed in. Default :id.
  • manage_path : Default associations to traverse for manage/5. Also, this is the preload argument when true is used.
  • prefilters - List of atoms indicating which fields should be prefiltered on. This means that separate indexes will be managed for each unique value for each of these fields, across all records of this entity type. Each two-element tuple has the field name atom and a keyword list of options.
  • query_fn : Optional function which takes a queryable and returns a queryable. This allows for extra query logic to be added such as populating virtual fields. Invoked by manage/5 when the association is needed.
  • :subscribe and unsubscribe : Functions which take a record's ID and manage the subscription. These must both be declared or neither.

tips

Tips

If you want to import Ecto.Query, you'll find that its preload/3 conflicts with Managed. Since Managed will use the repo as a fallback, you can exclude it this way.

defmodule MyModule do
  use Indexed.Managed
  import Ecto.Query, except: [preload: 2, preload: 3]
end

Link to this section Summary

Types

An association spec defines an association to another entity. It is used to build the preload function among other things.

Assoc spec as provided in the managed declaration. See assoc_spec/0.

A map of field names to assoc specs.

Used to explain the parent entity when processing its has_many relationship. Either {:top, name} where name is the top-level entity name OR nil for a parent with a :one association OR a tuple with

Preload option as passed to functions such as get/4. true means to follow the default provided by the entity's :manage_path.

Allows for a table namespace to be given when no more is needed.

For convenience, state is also accepted within a wrapping map.

t()
  • children : Map with assoc field name keys assoc_spec_opt/0 values. When this entity is managed, all children will also be managed and so on, recursively.
  • fields : Used to build the index. See Managed.Entity.t/0.
  • id_key : Used to get a record id. See Managed.Entity.t/0.
  • lookups : See t:Managed.Entity.t/0. Use these with "get_by" functions.
  • query : Optional function which takes a queryable and returns a queryable. This allows for extra query logic to be added such as populating virtual fields. Invoked by manage/5 when the association is needed.
  • manage_path : Default associations to traverse for manage/5.
  • module : The struct module which will be used for the records.
  • name : Atom name of the managed entity.
  • prefilters : Used to build the index. See Managed.Entity.t/0.
  • subscribe : 1-arity function which subscribes to changes by id.
  • tracked : True if another entity has a :one assoc to this. Internal.
  • unsubscribe : 1-arity function which unsubscribes to changes by id.

Functions

Loads initial data into index.

Invoke Indexed.drop/3 with a wrapped state for convenience.

Invoke Indexed.get/3. State may be wrapped in a map under :managed key.

Invoke Indexed.get_lookup/3 with a wrapped state for convenience.

Invoke Indexed.get_view/3 with a wrapped state for convenience.

Add, remove or update one or more managed records.

List name records using the Paginator interface.

Preload associations recursively.

Create ETS tables, init fresh state, without loading data.

Invoke Indexed.put/3 with a wrapped state for convenience.

Link to this section Types

@type add_or_rm() :: :add | :rm
@type assoc_spec() ::
  {:one, entity_name :: atom(), id_key :: atom()}
  | {:many, entity_name :: atom(), pf_key :: atom() | nil, order_hint()}
  | {:repo, assoc_field :: atom(), managed :: t()}

An association spec defines an association to another entity. It is used to build the preload function among other things.

  • {:one, entity_name, id_key} - Preload function should get a record of entity_name with id matching the id found under id_key of the record.
  • {:many, entity_name, pf_key, order_hint} - Preload function should use Indexed.get_records/4. If pf_key is not null, it will be replaced with {pfkey, id} where id is the record's id.
  • {:repo, key, managed} - Preload function should use Repo.get/2 with the assoc's module and the id in the foreign key field for key in the record. This is the default when a child/assoc_spec isn't defined for an assoc.
@type assoc_spec_opt() ::
  atom()
  | assoc_spec()
  | {:many, entity_name :: atom()}
  | {:many, entity_name :: atom(), pf_key :: atom() | nil}

Assoc spec as provided in the managed declaration. See assoc_spec/0.

This is always normalized to assoc_spec/0 at compile time. Missing pieces are filled via Ecto.Schema reflection.

@type children() :: %{required(atom()) => assoc_spec()}

A map of field names to assoc specs.

@type data_opt() :: Indexed.Actions.Warm.data_opt()
@type id() :: Indexed.id()
@type id_key() :: atom() | (record() -> id())
@type managed_or_name() :: t() | atom()
@type order_hint() :: Indexed.order_hint()
@type parent_info() ::
  :top | {parent_name :: atom(), id(), path_entry :: atom()} | nil

Used to explain the parent entity when processing its has_many relationship. Either {:top, name} where name is the top-level entity name OR nil for a parent with a :one association OR a tuple with

  1. Parent entity name.
  2. ID of the parent.
  3. Field name which would have the list of :many children if loaded.
@type path() :: atom() | list()
@type prefilter() :: Indexed.prefilter()
@type preloads() :: atom() | list()
@type preloads_option() :: preloads() | true

Preload option as passed to functions such as get/4. true means to follow the default provided by the entity's :manage_path.

@type record() :: Indexed.record()
@type record_or_list() :: [record()] | record() | nil
@type state() :: Indexed.Managed.State.t()
@type state_or_module() :: state_or_wrapped() | module()

Allows for a table namespace to be given when no more is needed.

@type state_or_wrapped() ::
  state() | %{:managed => state() | nil, optional(any()) => any()}

For convenience, state is also accepted within a wrapping map.

@type t() :: %Indexed.Managed{
  children: children(),
  fields: [atom() | Indexed.Entity.field()],
  id_key: id_key(),
  lookups: [Indexed.Entity.field_name()],
  manage_path: path(),
  module: module(),
  name: atom(),
  prefilters: [atom() | keyword()] | nil,
  query: (Ecto.Queryable.t() -> Ecto.Queryable.t()) | nil,
  subscribe: (Ecto.UUID.t() -> :ok | {:error, any()}) | nil,
  tracked: boolean(),
  unsubscribe: (Ecto.UUID.t() -> :ok | {:error, any()}) | nil
}
  • children : Map with assoc field name keys assoc_spec_opt/0 values. When this entity is managed, all children will also be managed and so on, recursively.
  • fields : Used to build the index. See Managed.Entity.t/0.
  • id_key : Used to get a record id. See Managed.Entity.t/0.
  • lookups : See t:Managed.Entity.t/0. Use these with "get_by" functions.
  • query : Optional function which takes a queryable and returns a queryable. This allows for extra query logic to be added such as populating virtual fields. Invoked by manage/5 when the association is needed.
  • manage_path : Default associations to traverse for manage/5.
  • module : The struct module which will be used for the records.
  • name : Atom name of the managed entity.
  • prefilters : Used to build the index. See Managed.Entity.t/0.
  • subscribe : 1-arity function which subscribes to changes by id.
  • tracked : True if another entity has a :one assoc to this. Internal.
  • unsubscribe : 1-arity function which unsubscribes to changes by id.

Link to this section Functions

Link to this function

create_view(state, name, fingerprint, opts \\ [])

View Source
@spec create_view(state_or_wrapped(), atom(), Indexed.View.fingerprint(), keyword()) ::
  {:ok, Indexed.View.t()} | :error
Link to this function

do_warm(state, name, data, path, opts)

View Source
@spec do_warm(state(), atom(), data_opt(), path(), keyword()) :: state()

Loads initial data into index.

@spec drop(state_or_wrapped(), atom(), id()) :: :ok | :error

Invoke Indexed.drop/3 with a wrapped state for convenience.

Link to this function

get(som, name, id, preloads \\ nil)

View Source
@spec get(state_or_module(), atom(), id(), preloads_option()) :: any()

Invoke Indexed.get/3. State may be wrapped in a map under :managed key.

If preloads is true, use the entity's default path.

Link to this function

get_by(som, name, field, value, preloads \\ nil)

View Source
@spec get_by(state_or_module(), atom(), atom(), any(), preloads_option()) :: [
  record()
]
Link to this function

get_ids_by(som, name, field, value)

View Source
@spec get_ids_by(state_or_module(), atom(), atom(), any()) :: [record()]
Link to this function

get_index(som, name, prefilter \\ nil, order_hint \\ nil)

View Source

Invoke Indexed.get_index/4 with a wrapped state for convenience.

Link to this function

get_lookup(som, name, field)

View Source
@spec get_lookup(state_or_module(), atom(), atom()) :: Indexed.lookup() | nil

Invoke Indexed.get_lookup/3 with a wrapped state for convenience.

Link to this function

get_records(som, name, prefilter \\ nil, order_hint \\ nil, preloads \\ nil)

View Source
@spec get_records(
  state_or_module(),
  atom(),
  prefilter(),
  order_hint() | nil,
  preloads()
) :: [record()]

Invoke Indexed.get_records/4 with a wrapped state for convenience.

Link to this function

get_uniques_list(som, name, prefilter \\ nil, field)

View Source
@spec get_uniques_list(state_or_module(), atom(), prefilter(), atom()) :: list() | nil

Invoke Indexed.get_uniques_list/4.

Link to this function

get_uniques_map(state, name, prefilter \\ nil, field)

View Source
@spec get_uniques_map(state_or_wrapped(), atom(), prefilter(), atom()) ::
  Indexed.UniquesBundle.counts_map() | nil

Invoke Indexed.get_uniques_map/4.

Link to this function

get_view(som, name, fingerprint)

View Source
@spec get_view(state_or_module(), atom(), Indexed.View.fingerprint()) ::
  Indexed.View.t() | nil

Invoke Indexed.get_view/3 with a wrapped state for convenience.

Link to this function

manage(state, mon, orig, new, path \\ nil)

View Source
@spec manage(
  state_or_wrapped(),
  managed_or_name(),
  :insert | :update | :delete | id() | record_or_list(),
  id() | record_or_list(),
  path()
) :: state_or_wrapped()

Add, remove or update one or more managed records.

The entity name atom should be declared as managed.

Arguments 3 and 4 can take one of the following forms:

  • :insert and the new record: The given record and associations are added to the cache.
  • :update and the newly updated record or its ID: The given record and associations are updated in the cache. Raises if we don't hold the record.
  • :upsert and the newly updated record: The given record and associations are updated in the cache. If we don't hold the record, insert.
  • :delete and the record or ID to remove from cache. Raises if we don't hold the record.
  • If the original and new records are already known, they may also be supplied directly.

Records and their associations are added, removed or updated in the cache by ID.

path is formatted the same as Ecto's preload option and it specifies which fields and how deeply to traverse when updating the in-memory cache. If path is not supplied, the entity's :manage_path will be used. (Supply [] to override this and avoid managing associations.)

Link to this macro

managed(name, module_or_opts \\ nil, opts \\ [])

View Source (macro)

Define a managed entity.

@spec managed_stat(state()) :: keyword()
Link to this function

paginate(som, name, params)

View Source
@spec paginate(state_or_module(), atom(), keyword()) :: Paginator.Page.t() | nil

List name records using the Paginator interface.

Link to this function

preload(record_or_list, som, preloads)

View Source
@spec preload(map() | [map()] | nil, state_or_module(), preloads()) ::
  [map()] | map() | nil

Preload associations recursively.

@spec prewarm(state_or_wrapped(), module()) :: state_or_wrapped()

Create ETS tables, init fresh state, without loading data.

Link to this function

put(state, name, record)

View Source
@spec put(state_or_wrapped(), atom(), record()) :: :ok

Invoke Indexed.put/3 with a wrapped state for convenience.