defmodule Sanity.Sync do @moduledoc """ For syncing content from Sanity CMS to Ecto. """ import Ecto.Query alias Sanity.Sync.Doc import UnsafeAtomizeKeys defp repo, do: Application.fetch_env!(:sanity_sync, :repo) @doc """ Gets a single document. Returns `nil` if document does not exist. """ def get_doc(id) do case repo().get(Doc, id) do nil -> nil %Doc{doc: doc} -> unsafe_atomize_keys(doc) end end @doc """ Gets a single document. Raises `Ecto.NoResultsError` if document does not exist. """ def get_doc!(id) do repo().get!(Doc, id).doc |> unsafe_atomize_keys() end @doc """ Fetches a single document from Sanity. If the document exists then `upsert_sanity_doc!/2` will be called. If the document doesn't exist, then the `Sanity.Sync.Doc` for that document will be deleted. This function can be called when a webhook is received to sync a document. ## Options * `callback` - Callback function that will be called after the document is upserted. It will be passed a map like `%{doc: doc, repo: repo}`. This callback is not called when the record is deleted. * `sanity_config` - Sanity configuration. See `Sanity.request/2`. """ def sync(id, opts) when is_binary(id) do opts = Keyword.validate!(opts, [:callback, :sanity_config]) """ *[_id == $id] """ |> Sanity.query(%{id: id}) |> request!(Keyword.fetch!(opts, :sanity_config)) |> Sanity.result!() |> case do [doc] -> doc |> unsafe_atomize_keys(&Inflex.underscore/1) |> upsert_sanity_doc!(opts) [] -> repo().delete_all(from d in Doc, where: d.id == ^id) end end @doc """ Fetches all documents from Sanity and calls `upsert_sanity_doc!/2`. ## Options * `callback` - Callback function that will be called after each document is upserted. It will be passed a map like `%{doc: doc, repo: repo}`. * `sanity_config` - Sanity configuration. See `Sanity.request/2`. * `types` - List of types to sync. If omitted, all types will be synced. """ def sync_all(opts) do opts = Keyword.validate!(opts, [:callback, :sanity_config, :types]) """ *[_type in $types && !(_id in path("drafts.**"))] """ |> Sanity.query(%{types: Keyword.fetch!(opts, :types)}) |> request!(Keyword.fetch!(opts, :sanity_config)) |> Sanity.result!() |> Enum.map(fn doc -> unsafe_atomize_keys(doc, &Inflex.underscore/1) end) |> Enum.each(&upsert_sanity_doc!(&1, opts)) # TODO paginate end @doc """ Upserts a sanity document. """ def upsert_sanity_doc!(%{_id: id, _type: type} = doc, opts \\ []) do Doc.changeset(%Doc{}, %{doc: doc, id: id, type: type}) |> repo().insert!(conflict_target: :id, on_conflict: :replace_all) |> tap(fn _ -> case Keyword.fetch(opts, :callback) do {:ok, cb} -> cb.(%{doc: doc, repo: repo()}) :error -> nil end end) end defp request!(request, config) do Application.get_env(:sanity_sync, :sanity_client, Sanity).request!(request, config) end end