From b8e6ea11ed64c3980d49fc17a8c0ddeac740bbca Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Sat, 11 May 2024 14:42:09 -0600 Subject: [PATCH] Perspectives (#83) * 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` --- CHANGELOG.md | 8 +++- lib/sanity.ex | 20 +++++----- test/integration_test.exs | 81 +++++++++++++++++++++++++++++++-------- test/sanity_test.exs | 46 ++++++++-------------- 4 files changed, 97 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34459a4..a9c9810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/sanity.ex b/lib/sanity.ex index b534aef..babd522 100644 --- a/lib/sanity.ex +++ b/lib/sanity.ex @@ -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 @@ -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, @@ -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!() @@ -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). diff --git a/test/integration_test.exs b/test/integration_test.exs index 2bd2513..2e1406c 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -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"}>) diff --git a/test/sanity_test.exs b/test/sanity_test.exs index 277b0cb..1c3b53e 100644 --- a/test/sanity_test.exs +++ b/test/sanity_test.exs @@ -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"}]}} @@ -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: []", @@ -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}"}) @@ -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\"" } @@ -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"}]}}