Skip to content

Commit a6fd9b4

Browse files
Scott BennettScott Bennett
Scott Bennett
authored and
Scott Bennett
committed
add cms and query modules
1 parent 9ca1a40 commit a6fd9b4

File tree

8 files changed

+271
-7
lines changed

8 files changed

+271
-7
lines changed

.tool-versions

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
elixir 1.11.3
2-
erlang 23.2.3
1+
elixir 1.14.2-otp-25
2+
erlang 25.1.2

config/config.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use Mix.Config
1+
import Config
22

33
config :ex_sanity,
44
file_base: "https://cdn.sanity.io",
@@ -8,4 +8,4 @@ config :ex_sanity,
88
version: "xxx",
99
endpoint: "xxx"
1010

11-
import_config "#{Mix.env}.exs"
11+
import_config "#{Mix.env()}.exs"

config/test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use Mix.Config
1+
import Config
22

33
config :ex_sanity,
44
project_id: "123",

lib/ex_sanity/cms.ex

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
defmodule ExSanity.CMS do
2+
@moduledoc """
3+
Inspired by Ecto.Repo. Runs queries against the Sanity CMS and returns the
4+
result.
5+
6+
Currently only supports "read" type queries. Create/update queries are on
7+
the roadmap.
8+
"""
9+
alias ExSanity.Client
10+
alias ExSanity.Query
11+
12+
def get(query, id) do
13+
query_string = Query.build(query, id)
14+
15+
{:ok, response} = Client.query(query_string)
16+
17+
[document | _] = response.body["result"]
18+
19+
document
20+
end
21+
22+
def one(query) do
23+
query_string = Query.build(query)
24+
25+
{:ok, response} = Client.query(query_string)
26+
27+
[document | _] = response.body["result"]
28+
29+
document
30+
end
31+
32+
def all(query) do
33+
query_string = Query.build(query)
34+
35+
{:ok, response} = Client.query(query_string)
36+
37+
response.body["result"]
38+
end
39+
end

lib/ex_sanity/query.ex

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
defmodule ExSanity.Query do
2+
@moduledoc false
3+
defstruct types: [], preloads: [], filters: [], projections: [], orderings: [], slice: ""
4+
5+
def from(type) when is_atom(type) do
6+
%__MODULE__{
7+
types: [type]
8+
}
9+
end
10+
11+
def from(types) do
12+
%__MODULE__{
13+
types: types
14+
}
15+
end
16+
17+
def select(query, fields) do
18+
%{query | projections: query.projections ++ fields}
19+
end
20+
21+
def filter(query, filter) do
22+
%{query | filters: query.filters ++ [filter]}
23+
end
24+
25+
def preload(query, preloads) do
26+
%{query | preloads: preloads}
27+
end
28+
29+
def order(query, order) do
30+
%{query | orderings: query.orderings ++ [order]}
31+
end
32+
33+
def slice(query, slice) do
34+
%{query | slice: slice}
35+
end
36+
37+
def build(query, id) do
38+
query = filter(query, ~s(_id == "#{id}"))
39+
build(query)
40+
end
41+
42+
def build(%{types: types, filters: [], orderings: [], slice: "", projections: [], preloads: []}) do
43+
~s(*[#{build_types(types)}])
44+
end
45+
46+
def build(%{types: "*", filters: filters}) do
47+
~s(*[#{build_filters(filters)}])
48+
end
49+
50+
def build(%{
51+
types: types,
52+
filters: filters,
53+
orderings: [],
54+
slice: "",
55+
projections: [],
56+
preloads: []
57+
}) do
58+
~s(*[#{build_types(types)}#{build_filters(filters)}])
59+
end
60+
61+
def build(%{
62+
types: types,
63+
filters: filters,
64+
orderings: orderings,
65+
slice: slice,
66+
projections: [],
67+
preloads: []
68+
}) do
69+
~s(*[#{build_types(types)}#{build_filters(filters)}]#{build_orderings(orderings)}#{build_slice(slice)})
70+
end
71+
72+
def build(query) do
73+
~s(*[#{build_types(query.types)}#{build_filters(query.filters)}]#{build_orderings(query.orderings)}#{build_slice(query.slice)}{#{build_projections(query.projections)}#{build_preloads(query.preloads)}})
74+
end
75+
76+
defp build_types("*"), do: ""
77+
78+
defp build_types([type]) do
79+
~s(_type == "#{type}")
80+
end
81+
82+
defp build_types(types) do
83+
~s(_type in ["#{Enum.join(types, "\", \"")}"])
84+
end
85+
86+
defp build_filters([filter]) do
87+
~s(#{filter})
88+
end
89+
90+
defp build_filters(filters) do
91+
Enum.reduce(filters, "", fn filter, acc ->
92+
acc <> ~s( && #{filter})
93+
end)
94+
end
95+
96+
defp build_projections(projections) do
97+
Enum.join(projections, ", ")
98+
end
99+
100+
defp build_preloads(preloads) do
101+
Enum.reduce(preloads, "", fn preload, acc ->
102+
acc <> ~s(, #{Atom.to_string(preload)}[]->)
103+
end)
104+
end
105+
106+
defp build_orderings(orderings) do
107+
Enum.reduce(orderings, "", fn {field, direction}, acc ->
108+
acc <> ~s( | order(#{field} #{Atom.to_string(direction)}\))
109+
end)
110+
end
111+
112+
defp build_slice(""), do: ""
113+
114+
defp build_slice(slice) do
115+
~s([#{slice}])
116+
end
117+
end

mix.exs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule ExSanity.MixProject do
55
[
66
app: :ex_sanity,
77
version: "0.1.0",
8-
elixir: "~> 1.11",
8+
elixir: "~> 1.14.2",
99
start_permanent: Mix.env() == :prod,
1010
deps: deps()
1111
]
@@ -24,7 +24,8 @@ defmodule ExSanity.MixProject do
2424
{:jason, ">= 1.1.0"},
2525
{:httpoison, ">= 1.8.0"},
2626
{:phoenix_html, ">= 2.14.2"},
27-
{:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false}
27+
{:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false},
28+
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
2829
]
2930
end
3031
end

mix.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
%{
2+
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
23
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
4+
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
35
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
46
"hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
57
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},

test/ex_sanity/query_test.exs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
defmodule ExSanity.QueryTest do
2+
use ExUnit.Case, async: true
3+
4+
import ExSanity.Query
5+
6+
describe "from/1" do
7+
test "sets document type" do
8+
query = from(:foo)
9+
10+
assert query.types == [:foo]
11+
end
12+
end
13+
14+
describe "select/2" do
15+
test "sets projections" do
16+
query =
17+
from(:foo)
18+
|> select([:bar])
19+
20+
assert query.projections == [:bar]
21+
end
22+
end
23+
24+
describe "Sanity Examples" do
25+
test "Filters" do
26+
assert from("*") |> build() == ~s(*[])
27+
assert from(:movie) |> build() == ~s(*[_type == "movie"])
28+
assert from("*") |> build("abc.123") == ~s(*[_id == "abc.123"])
29+
assert from([:movie, :person]) |> build() == ~s(*[_type in ["movie", "person"]])
30+
31+
assert from(:movie)
32+
|> filter("popularity > 15")
33+
|> filter("releaseDate > \"2016-04-25\"")
34+
|> build() ==
35+
~s(*[_type == "movie" && popularity > 15 && releaseDate > "2016-04-25"])
36+
37+
# May need to wait on "OR" filters
38+
# assert from(:movie)
39+
# |> filter("popularity > 15")
40+
# |> or_filter("releaseDate > \"2016-04-25\"")
41+
# |> build() ==
42+
# ~s(*[_type == "movie" && (popularity > 15 || releaseDate > "2016-04-25"\)])
43+
44+
# *[popularity < 15] // less than
45+
# *[popularity > 15] // greater than
46+
# *[popularity <= 15] // less than or equal
47+
# *[popularity >= 15] // greater than or equal
48+
# *[popularity == 15]
49+
# *[releaseDate != "2016-04-27"] // not equal
50+
# *[!(releaseDate == "2016-04-27")] // not equal
51+
# *[!(releaseDate != "2016-04-27")] // even equal via double negatives "not not equal"
52+
# *[dateTime(_updatedAt) > dateTime('2018-04-20T20:43:31Z')] // Use zulu-time when comparing datetimes to strings
53+
# *[dateTime(_updatedAt) > dateTime(now()) - 60*60*24*7] // Updated within the past week
54+
# *[name < "Baker"] // Records whose name precedes "Baker" alphabetically
55+
# *[awardWinner == true] // match boolean
56+
# *[awardWinner] // true if awardWinner == true
57+
# *[!awardWinner] // true if awardWinner == false
58+
# *[defined(awardWinner)] // has been assigned an award winner status (any kind of value)
59+
# *[!defined(awardWinner)] // has not been assigned an award winner status (any kind of value)
60+
# *[title == "Aliens"]
61+
# *[title in ["Aliens", "Interstellar", "Passengers"]]
62+
# *[_id in path("a.b.c.*")] // _id matches a.b.c.d but not a.b.c.d.e
63+
# *[_id in path("a.b.c.**")] // _id matches a.b.c.d, and also a.b.c.d.e.f.g, but not a.b.x.1
64+
# *[!(_id in path("drafts.**"))] // _id matches anything that is not under the drafts-path
65+
# *["yolo" in tags] // documents that have the string "yolo" in the array "tags"
66+
# *[status in ["completed", "archived"]] // the string field status is either == "completed" or "archived"
67+
# *["person_sigourney-weaver" in castMembers[].person._ref] // Any document having a castMember referencing sigourney as its person
68+
# *[slug.current == "some-slug"] // nested properties
69+
# *[count((categories[]->slug.current)[@ in ["action", "thriller"]]) > 0] // documents that reference categories with slugs of "action" or "thriller"
70+
# *[count((categories[]->slug.current)[@ in ["action", "thriller"]]) == 2]
71+
end
72+
73+
test "ordering and slicing" do
74+
assert from(:movie)
75+
|> order({:_createdAt, :asc})
76+
|> build() == ~s(*[_type == "movie"] | order(_createdAt asc\))
77+
78+
assert from(:movie)
79+
|> order({:releaseDate, :desc})
80+
|> order({:_createdAt, :asc})
81+
|> build() ==
82+
~s(*[_type == "movie"] | order(releaseDate desc\) | order(_createdAt asc\))
83+
84+
# FUTURE: support mixed ordering
85+
# *[_type == "todo"] | order(priority desc, _updatedAt desc)
86+
87+
assert from(:movie)
88+
|> order({:_createdAt, :asc})
89+
|> slice("0")
90+
|> build() == ~s(*[_type == "movie"] | order(_createdAt asc\)[0])
91+
92+
# *[_type == "movie"] | order(_createdAt desc)[0]
93+
94+
# *[_type == "movie"] | order(_createdAt asc)[0..9]
95+
96+
# *[_type == "movie"][0..9] | order(_createdAt asc)
97+
98+
# *[_type == "movie"] | order(_createdAt asc) [$start..$end]
99+
100+
# *[_type == "movie"] | order(title asc)
101+
102+
# *[_type == "movie"] | order(lower(title) asc)
103+
end
104+
end
105+
end

0 commit comments

Comments
 (0)