Skip to content

Commit

Permalink
Perspectives (#83)
Browse files Browse the repository at this point in the history
* integration test to prepare for perspectives

* Update integration_test.exs

* perspectives

* Update CHANGELOG.md

* Update CHANGELOG.md

* document how to use perspectives with `Sanity.query/3`
  • Loading branch information
balexand authored May 11, 2024
1 parent 154f798 commit b8e6ea1
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 58 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Add support for [Perspectives](https://www.sanity.io/blog/introducing-perspectives-sanity-previews) (https://github.com/balexand/sanity/pull/83).

### Removed
- (BREAKING) The `:drafts` option has been removed and passing it will result in an error. The `:perspective` option should be used instead. The default behavior is the same so you will only need to update your code if you are explicitly passing the `:drafts` option.

### Changed
- (BREAKING) Switch HTTP client from `finch` to `req` (https://github.com/balexand/sanity/pull/81). This introduces the following breaking changes:
- The `headers` field of the `Sanity.Response` now returns a map instead of a list of tuples. See https://hexdocs.pm/req/changelog.html#change-headers-to-be-maps for details.
- The `:http_options` option for `Sanity.request/2` is now passed to `Req.request/1` instead of to Finch. The available options will be different.
- The `:max_attempts` and `:retry_delay` options have been removed from `Sanity.request/2`. `Req` handles retries for us.
- The `:max_attempts` and `:retry_delay` options have been removed from `Sanity.request/2`. Passing these options will now result in an error. `Req` handles retries for us.
- The `source` field in the `Sanity.Error` may contain different values. See [`Sanity.Error`](https://hexdocs.pm/sanity/Sanity.Error.html) for details.

## [1.3.0] - 2023-07-19
Expand Down
20 changes: 10 additions & 10 deletions lib/sanity.ex
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ defmodule Sanity do
Generates a request to the [Query](https://www.sanity.io/docs/http-query) endpoint. Requests to
this endpoint may be authenticated or unauthenticated. Unauthenticated requests to a dataset
with private visibility will succeed but will not return any documents.
[Perspectives](https://www.sanity.io/docs/perspectives) can be used by passing the `"perspective"`
query param like, `Sanity.query("*", %{}, perspective: "previewDrafts")`. This function does not
set a perspective by default, which is equivalent to a perspective of `"raw"`.
"""
@spec query(String.t(), keyword() | map(), keyword() | map()) :: Request.t()
def query(query, variables \\ %{}, query_params \\ []) do
Expand Down Expand Up @@ -281,11 +285,11 @@ defmodule Sanity do
doc:
~S'Number of results to fetch per request. The Sanity docs say: "In the general case, we recommend a batch size of no more than 5,000. If your documents are very large, a smaller batch size is better."'
],
drafts: [
type: {:in, [:exclude, :include, :only]},
default: :exclude,
perspective: [
type: :string,
default: "published",
doc:
"Use `:exclude` to exclude drafts, `:include` to include drafts along with published docs, or `:only` to fetch drafts and not published documents."
~S'Perspective to use. Common values are `"published"`, `"previewDrafts"`, and `"raw"`. See the [docs](https://www.sanity.io/docs/perspectives) for details.'
],
projection: [
type: :string,
Expand Down Expand Up @@ -355,14 +359,14 @@ defmodule Sanity do

defp stream_page(opts, page_query) do
query =
[opts[:query], drafts_query(opts[:drafts]), page_query]
[opts[:query], page_query]
|> Enum.filter(& &1)
|> Enum.map(&"(#{&1})")
|> Enum.join(" && ")

results =
"*[#{query}] | order(_id) [0..#{opts[:batch_size] - 1}] #{opts[:projection]}"
|> query(opts[:variables])
|> query(opts[:variables], perspective: opts[:perspective])
|> opts[:request_module].request!(opts[:request_opts])
|> result!()

Expand All @@ -373,10 +377,6 @@ defmodule Sanity do
end
end

defp drafts_query(:exclude), do: "!(_id in path('drafts.**'))"
defp drafts_query(:include), do: nil
defp drafts_query(:only), do: "_id in path('drafts.**')"

@doc """
Generates a request for the [asset endpoint](https://www.sanity.io/docs/http-api-assets).
Expand Down
81 changes: 64 additions & 17 deletions test/integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -127,26 +127,73 @@ defmodule Sanity.MutateIntegrationTest do
|> Sanity.request(Keyword.put(config, :cdn, true))
end

test "stream", %{config: config} do
type = "streamItem#{:rand.uniform(1_000_000)}"

Sanity.mutate(Enum.map(1..5, &%{create: %{_type: type, title: "item #{&1}"}}))
|> Sanity.request(config)

assert [
%{"title" => "item 1"},
%{"title" => "item 2"},
%{"title" => "item 3"},
%{"title" => "item 4"},
%{"title" => "item 5"}
] =
Sanity.stream(query: "_type == '#{type}'", batch_size: 2, request_opts: config)
|> Enum.to_list()
|> Enum.sort_by(& &1["title"])
describe "stream" do
test "with multiple batches", %{config: config} do
type = "streamItem#{:rand.uniform(1_000_000)}"

Sanity.mutate(Enum.map(1..5, &%{create: %{_type: type, title: "item #{&1}"}}))
|> Sanity.request!(config)

Sanity.mutate([
%{create: %{_id: "drafts.a-#{:rand.uniform(1_000_000)}", _type: type, title: "my draft"}}
])
|> Sanity.request!(config)

assert [
%{"title" => "item 1"},
%{"title" => "item 2"},
%{"title" => "item 3"},
%{"title" => "item 4"},
%{"title" => "item 5"}
] =
Sanity.stream(query: "_type == '#{type}'", batch_size: 2, request_opts: config)
|> Enum.to_list()
|> Enum.sort_by(& &1["title"])
end

test "with perspectives", %{config: config} do
type = "streamItem#{:rand.uniform(1_000_000)}"
id = "my-id-#{:rand.uniform(1_000_000)}"

Sanity.mutate([
%{create: %{_id: id, _type: type, title: "published item"}},
%{create: %{_id: "drafts.#{id}", _type: type, title: "draft item"}}
])
|> Sanity.request!(config)

# Published (default)
assert [%{"_id" => ^id, "title" => "published item"}] =
Sanity.stream(
query: "_type == '#{type}'",
request_opts: config
)
|> Enum.to_list()

# Preview drafts
assert [%{"_id" => ^id, "title" => "draft item"}] =
Sanity.stream(
query: "_type == '#{type}'",
perspective: "previewDrafts",
request_opts: config
)
|> Enum.to_list()

# Raw
assert [
%{"_id" => "drafts." <> ^id, "title" => "draft item"},
%{"_id" => ^id, "title" => "published item"}
] =
Sanity.stream(
query: "_type == '#{type}'",
perspective: "raw",
request_opts: config
)
|> Enum.to_list()
end
end

test "timeout error", %{config: config} do
config = Keyword.put(config, :http_options, receive_timeout: 0, retry_log_level: false)
config = Keyword.put(config, :http_options, receive_timeout: 0, retry: false)

assert_raise Sanity.Error, "%Mint.TransportError{reason: :timeout}", fn ->
Sanity.query(~S<{"hello": "world"}>)
Expand Down
46 changes: 16 additions & 30 deletions test/sanity_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ defmodule SanityTest do
test "opts[:drafts] == :exclude (default)" do
Mox.expect(MockSanity, :request!, fn %Request{query_params: query_params}, _ ->
assert query_params == %{
"query" => "*[(!(_id in path('drafts.**')))] | order(_id) [0..999] { ... }"
"query" => "*[] | order(_id) [0..999] { ... }",
"perspective" => "published"
}

%Response{body: %{"result" => [%{"_id" => "a"}]}}
Expand All @@ -267,40 +268,24 @@ defmodule SanityTest do
Sanity.stream(request_module: MockSanity, request_opts: @request_config) |> Enum.to_list()
end

test "opts[:drafts] == :include" do
test "opts[:perspective]" do
Mox.expect(MockSanity, :request!, fn %Request{query_params: query_params}, _ ->
assert query_params == %{
"query" => "*[] | order(_id) [0..999] { ... }"
"query" => "*[] | order(_id) [0..999] { ... }",
"perspective" => "previewDrafts"
}

%Response{body: %{"result" => [%{"_id" => "a"}]}}
end)

Sanity.stream(drafts: :include, request_module: MockSanity, request_opts: @request_config)
Sanity.stream(
perspective: "previewDrafts",
request_module: MockSanity,
request_opts: @request_config
)
|> Enum.to_list()
end

test "opts[:drafts] == :only" do
Mox.expect(MockSanity, :request!, fn %Request{query_params: query_params}, _ ->
assert query_params == %{
"query" => "*[(_id in path('drafts.**'))] | order(_id) [0..999] { ... }"
}

%Response{body: %{"result" => [%{"_id" => "a"}]}}
end)

Sanity.stream(drafts: :only, request_module: MockSanity, request_opts: @request_config)
|> Enum.to_list()
end

test "opts[:drafts] == :invalid" do
assert_raise NimbleOptions.ValidationError,
"invalid value for :drafts option: expected one of [:exclude, :include, :only], got: :invalid",
fn ->
Sanity.stream(drafts: :invalid, request_opts: @request_config)
end
end

test "opts[:variables] invalid" do
assert_raise NimbleOptions.ValidationError,
"invalid value for :variables option: expected map, got: []",
Expand All @@ -318,7 +303,8 @@ defmodule SanityTest do
test "pagination" do
Mox.expect(MockSanity, :request!, fn %Request{query_params: query_params}, _ ->
assert query_params == %{
"query" => "*[(!(_id in path('drafts.**')))] | order(_id) [0..4] { ... }"
"query" => "*[] | order(_id) [0..4] { ... }",
"perspective" => "published"
}

results = Enum.map(1..5, &%{"_id" => "doc-#{&1}"})
Expand All @@ -327,8 +313,8 @@ defmodule SanityTest do

Mox.expect(MockSanity, :request!, fn %Request{query_params: query_params}, _ ->
assert query_params == %{
"query" =>
"*[(!(_id in path('drafts.**'))) && (_id > $pagination_last_id)] | order(_id) [0..4] { ... }",
"query" => "*[(_id > $pagination_last_id)] | order(_id) [0..4] { ... }",
"perspective" => "published",
"$pagination_last_id" => "\"doc-5\""
}

Expand Down Expand Up @@ -357,8 +343,8 @@ defmodule SanityTest do
Mox.expect(MockSanity, :request!, fn %Request{query_params: query_params}, _request_opts ->
assert query_params == %{
"$type" => "\"page\"",
"query" =>
"*[(_type == $type) && (!(_id in path('drafts.**')))] | order(_id) [0..999] { _id, title }"
"query" => "*[(_type == $type)] | order(_id) [0..999] { _id, title }",
"perspective" => "published"
}

%Response{body: %{"result" => [%{"_id" => "a", "title" => "home"}]}}
Expand Down

0 comments on commit b8e6ea1

Please sign in to comment.