From b04d1033bba55f52243a236edd72fd2913096de5 Mon Sep 17 00:00:00 2001 From: Arno Dirlam Date: Sat, 13 Jun 2020 13:50:53 +0200 Subject: [PATCH 1/5] Implement id_for/1 using new Weaver.Node protocol --- lib/weaver.ex | 2 +- lib/weaver/absinthe/schema.ex | 10 +++++++++- lib/weaver/node.ex | 22 ++++++++++++++++++++++ lib/weaver/resolvers.ex | 6 +----- lib/weaver/step.ex | 2 +- test/node_test.exs | 21 +++++++++++++++++++++ 6 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 lib/weaver/node.ex create mode 100644 test/node_test.exs diff --git a/lib/weaver.ex b/lib/weaver.ex index 3c522fd..2f2da8b 100644 --- a/lib/weaver.ex +++ b/lib/weaver.ex @@ -24,7 +24,7 @@ defmodule Weaver do } def new(id) when is_binary(id), do: %__MODULE__{id: id} - def from(obj), do: new(Weaver.Resolvers.id_for(obj)) + def from(obj), do: new(Weaver.Node.id_for(obj)) end defmodule Marker do diff --git a/lib/weaver/absinthe/schema.ex b/lib/weaver/absinthe/schema.ex index 741c8a1..e6b8e58 100644 --- a/lib/weaver/absinthe/schema.ex +++ b/lib/weaver/absinthe/schema.ex @@ -7,6 +7,14 @@ defmodule Weaver.Absinthe.Schema do alias ExTwitter.Model.{Tweet, User} alias Weaver.Resolvers + defimpl Weaver.Node, for: Tweet do + def id_for(tweet), do: "Tweet:#{tweet.id_str}" + end + + defimpl Weaver.Node, for: User do + def id_for(user), do: "TwitterUser:#{user.screen_name}" + end + interface :node do field(:id, non_null(:id)) end @@ -87,6 +95,6 @@ defmodule Weaver.Absinthe.Schema do end defp weaver_id(obj, _, _) do - {:ok, Resolvers.id_for(obj)} + {:ok, Weaver.Node.id_for(obj)} end end diff --git a/lib/weaver/node.ex b/lib/weaver/node.ex new file mode 100644 index 0000000..2aec28e --- /dev/null +++ b/lib/weaver/node.ex @@ -0,0 +1,22 @@ +defprotocol Weaver.Node do + @fallback_to_any true + + @doc """ + Returns an identifier for a given term. + + The identifier must uniquely identify a record within the record's GraphQL type. + + It may be a String or number. + """ + @spec id_for(any()) :: binary() | number() + def id_for(term) +end + +defimpl Weaver.Node, for: Any do + def id_for(%{id: id}), do: id + def id_for(%{"id" => id}), do: id + + def id_for(term) do + raise Protocol.UndefinedError, protocol: Weaver.Node, value: term + end +end diff --git a/lib/weaver/resolvers.ex b/lib/weaver/resolvers.ex index 9da8eb1..c8b2394 100644 --- a/lib/weaver/resolvers.ex +++ b/lib/weaver/resolvers.ex @@ -18,9 +18,6 @@ defmodule Weaver.Resolvers do @twitter_client.user(id) end - def id_for(obj = %User{}), do: "TwitterUser:#{obj.screen_name}" - def id_for(obj = %Tweet{}), do: "Tweet:#{obj.id_str}" - def end_marker(objs) when is_list(objs) do objs |> Enum.min_by(& &1.id) @@ -167,14 +164,13 @@ defmodule Weaver.Resolvers do ) end - case tweets do + case Enum.filter(tweets, & &1.retweeted_status) do [] -> {:done, []} tweets -> tweets = tweets - |> Enum.filter(& &1.retweeted_status) |> Enum.take(@api_take) {:continue, tweets, end_marker(tweets)} diff --git a/lib/weaver/step.ex b/lib/weaver/step.ex index ce9a561..6bc909a 100644 --- a/lib/weaver/step.ex +++ b/lib/weaver/step.ex @@ -92,7 +92,7 @@ defmodule Weaver.Step do defp before_marker?(obj, marker) do Resolvers.marker_val(obj) > marker.val && - Resolvers.id_for(obj) != marker.ref.id + Weaver.Node.id_for(obj) != marker.ref.id end def load_markers(step = %{next_chunk_start: val}, _opts, _cache, _parent_ref, _field) diff --git a/test/node_test.exs b/test/node_test.exs new file mode 100644 index 0000000..4ec5381 --- /dev/null +++ b/test/node_test.exs @@ -0,0 +1,21 @@ +defmodule Weaver.NodeTest do + use ExUnit.Case + + import Weaver.Node + + describe "id_for/1" do + test "uses :id field of a Map" do + assert id_for(%{name: "Carol", id: 12366}) == 12366 + end + + test "uses \"id\" field of a Map" do + assert id_for(%{"name" => "Carol", "id" => 12366}) == 12366 + end + + test "raises error if not implemented" do + assert_raise Protocol.UndefinedError, + ~r/^protocol Weaver.Node not implemented for \"Howdy!\" of type BitString./, + fn -> id_for("Howdy!") end + end + end +end From ba5bf353d164a159bb300fe239293127e0ac5e72 Mon Sep 17 00:00:00 2001 From: Arno Dirlam Date: Tue, 16 Jun 2020 23:10:01 +0200 Subject: [PATCH 2/5] Result phase: Pass parent_ref instead of full parent source --- lib/weaver/absinthe/phase/document/result.ex | 45 ++++++++++---------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/lib/weaver/absinthe/phase/document/result.ex b/lib/weaver/absinthe/phase/document/result.ex index b322d30..7e2390f 100644 --- a/lib/weaver/absinthe/phase/document/result.ex +++ b/lib/weaver/absinthe/phase/document/result.ex @@ -50,17 +50,17 @@ defmodule Weaver.Absinthe.Phase.Document.Result do end end - defp data([], parent, %{errors: [_ | _] = field_errors, emitter: emitter}, result) do + defp data([], parent_ref, %{errors: [_ | _] = field_errors, emitter: emitter}, result) do Result.add_errors( result, - Enum.map(field_errors, &{Ref.from(parent), field_name(emitter), &1}) + Enum.map(field_errors, &{parent_ref, field_name(emitter), &1}) ) end # Leaf - defp data(path, parent, %Leaf{value: nil, emitter: emitter} = field, result) do + defp data(path, parent_ref, %Leaf{value: nil, emitter: emitter} = field, result) do if on_path?(field, path) do - Result.add_data(result, {Ref.from(parent), field_name(emitter), nil}) + Result.add_data(result, {parent_ref, field_name(emitter), nil}) else result end @@ -70,7 +70,7 @@ defmodule Weaver.Absinthe.Phase.Document.Result do result end - defp data(path, parent, %{value: value, emitter: emitter} = field, result) do + defp data(path, parent_ref, %{value: value, emitter: emitter} = field, result) do if on_path?(field, path) do value = case Type.unwrap(emitter.schema_node.type) do @@ -81,7 +81,7 @@ defmodule Weaver.Absinthe.Phase.Document.Result do Type.Enum.serialize(schema_node, value) end - Result.add_data(result, {Ref.from(parent), field_name(emitter), value}) + Result.add_data(result, {parent_ref, field_name(emitter), value}) else result end @@ -93,41 +93,42 @@ defmodule Weaver.Absinthe.Phase.Document.Result do end defp data(path, nil, %{fields: fields, root_value: obj} = field, result) do - field_data(next_path(field, path), obj, fields, result) + field_data(next_path(field, path), Ref.from(obj), fields, result) end - defp data(path, parent, %{fields: fields, emitter: emitter, root_value: obj} = field, result) do + defp data( + path, + parent_ref, + %{fields: fields, emitter: emitter, root_value: obj} = field, + result + ) do next_path = next_path(field, path) if next_path do result = if next_path == [] do - Result.add_relation_data(result, {Ref.from(parent), field_name(emitter), [obj]}) + Result.add_relation_data(result, {parent_ref, field_name(emitter), [obj]}) else result end - field_data(next_path, obj, fields, result) + field_data(next_path, Ref.from(obj), fields, result) else result end end # List - defp data(path, parent, %{values: values} = field, result) do + defp data(path, parent_ref, %{values: values} = field, result) do if on_path?(field, path) do case path do [next, pos | rest] -> val = Enum.at(values, pos) - data([next | rest], parent, val, result) + data([next | rest], parent_ref, val, result) _ -> - Enum.reduce(values, result, fn val, acc -> - data(path, parent, val, acc) - end) + Enum.reduce(values, result, &data(path, parent_ref, &1, &2)) end - - # Enum.reduce(values, result, &data(path, parent, &1, &2)) else result end @@ -135,19 +136,19 @@ defmodule Weaver.Absinthe.Phase.Document.Result do defp field_data(_path, _parent, [], result), do: result - defp field_data(path, parent, [%Absinthe.Resolution{} | fields], result) do - field_data(path, parent, fields, result) + defp field_data(path, parent_ref, [%Absinthe.Resolution{} | fields], result) do + field_data(path, parent_ref, fields, result) end - defp field_data(path, parent, [field | fields], result) do + defp field_data(path, parent_ref, [field | fields], result) do result = if on_path?(field, path) do - data(path, parent, field, result) + data(path, parent_ref, field, result) else result end - field_data(path, parent, fields, result) + field_data(path, parent_ref, fields, result) end defp field_name(%{alias: nil, name: name}), do: name From 920c68898c46277eacab2dcbc5c5645e5a25714d Mon Sep 17 00:00:00 2001 From: Arno Dirlam Date: Tue, 16 Jun 2020 23:35:17 +0200 Subject: [PATCH 3/5] Fix code conventions --- test/node_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/node_test.exs b/test/node_test.exs index 4ec5381..9681d00 100644 --- a/test/node_test.exs +++ b/test/node_test.exs @@ -5,11 +5,11 @@ defmodule Weaver.NodeTest do describe "id_for/1" do test "uses :id field of a Map" do - assert id_for(%{name: "Carol", id: 12366}) == 12366 + assert id_for(%{name: "Carol", id: 12_366}) == 12_366 end test "uses \"id\" field of a Map" do - assert id_for(%{"name" => "Carol", "id" => 12366}) == 12366 + assert id_for(%{"name" => "Carol", "id" => 12_366}) == 12_366 end test "raises error if not implemented" do From 9af088276cc379a2496a2effb64ad5339c8f2d10 Mon Sep 17 00:00:00 2001 From: Arno Dirlam Date: Tue, 16 Jun 2020 23:37:46 +0200 Subject: [PATCH 4/5] Result phase: Use resolved __weaver_id to generate Ref if present --- lib/weaver/absinthe/phase/document/result.ex | 25 ++++---- test/timelines_test.exs | 61 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 test/timelines_test.exs diff --git a/lib/weaver/absinthe/phase/document/result.ex b/lib/weaver/absinthe/phase/document/result.ex index 7e2390f..61d75a9 100644 --- a/lib/weaver/absinthe/phase/document/result.ex +++ b/lib/weaver/absinthe/phase/document/result.ex @@ -92,27 +92,22 @@ defmodule Weaver.Absinthe.Phase.Document.Result do field_data(next_path(field, path), nil, fields, result) end - defp data(path, nil, %{fields: fields, root_value: obj} = field, result) do - field_data(next_path(field, path), Ref.from(obj), fields, result) + defp data(path, nil, %{fields: fields} = field, result) do + field_data(next_path(field, path), to_ref(field), fields, result) end - defp data( - path, - parent_ref, - %{fields: fields, emitter: emitter, root_value: obj} = field, - result - ) do + defp data(path, parent_ref, %{fields: fields, emitter: emitter} = field, result) do next_path = next_path(field, path) if next_path do result = if next_path == [] do - Result.add_relation_data(result, {parent_ref, field_name(emitter), [obj]}) + Result.add_relation_data(result, {parent_ref, field_name(emitter), [to_ref(field)]}) else result end - field_data(next_path, Ref.from(obj), fields, result) + field_data(next_path, to_ref(field), fields, result) else result end @@ -171,4 +166,14 @@ defmodule Weaver.Absinthe.Phase.Document.Result do end defp next_path(_field, []), do: [] + + defp to_ref(%{root_value: obj, fields: fields}) do + case fields do + [%{emitter: %{alias: "__weaver_id"}, value: id} | _] -> + %Ref{id: id} + + _ -> + Ref.from(obj) + end + end end diff --git a/test/timelines_test.exs b/test/timelines_test.exs new file mode 100644 index 0000000..27659bf --- /dev/null +++ b/test/timelines_test.exs @@ -0,0 +1,61 @@ +defmodule Weaver.TimelinesTest do + use Weaver.IntegrationCase, async: false + + @query """ + query { + node(id: "TwitterUser:elixirdigest") { + ... on TwitterUser { + screenName + tweets { + __weaver_id : id + __weaver_type : __typename + text + } + } + } + } + """ + + setup do + user = build(TwitterUser, screen_name: "elixirdigest") + + {:ok, user: user} + end + + test "works", %{user: user} do + user_ref = %Ref{id: "TwitterUser:#{user.screen_name}"} + # TWEETS + tweet1 = build(Tweet, id: 35) + tweet2 = build(Tweet, id: 21) + + @query + |> Weaver.prepare(Schema) + |> weave_initial(Twitter, :user, fn "elixirdigest" -> user end) + |> assert_data([ + {user_ref, "screenName", "elixirdigest"} + ]) + |> assert_meta([]) + |> assert_dispatched_paths([ + [%{name: "tweets"}, %{name: "node"}, %{name: nil}] + ]) + |> refute_next() + |> weave_dispatched(Twitter, :user_timeline, fn _ -> [tweet1, tweet2] end) + |> assert_data([ + {%Ref{id: "Tweet:#{tweet2.id}"}, "text", tweet2.full_text}, + {%Ref{id: "Tweet:#{tweet2.id}"}, "__weaver_type", "Tweet"}, + {%Ref{id: "Tweet:#{tweet2.id}"}, "__weaver_id", "Tweet:#{tweet2.id}"}, + {user_ref, "tweets", %Ref{id: "Tweet:#{tweet2.id}"}}, + {%Ref{id: "Tweet:#{tweet1.id}"}, "text", tweet1.full_text}, + {%Ref{id: "Tweet:#{tweet1.id}"}, "__weaver_type", "Tweet"}, + {%Ref{id: "Tweet:#{tweet1.id}"}, "__weaver_id", "Tweet:#{tweet1.id}"}, + {user_ref, "tweets", %Ref{id: "Tweet:#{tweet1.id}"}} + ]) + |> assert_meta([ + {:add, user_ref, "tweets", Marker.chunk_start("Tweet:#{tweet1.id}", tweet1.id)}, + {:add, user_ref, "tweets", Marker.chunk_end("Tweet:#{tweet2.id}", tweet2.id)} + ]) + |> assert_dispatched_paths([]) + |> assert_next_path([%{name: "tweets"}, %{name: "node"}, %{name: nil}]) + |> assert_next_state(%{prev_chunk_end: %Marker{val: 21}}) + end +end From 5bf9df66dd3c16b5730b62e1d63407052660cebb Mon Sep 17 00:00:00 2001 From: Arno Dirlam Date: Fri, 19 Jun 2020 18:01:07 +0200 Subject: [PATCH 5/5] Move reference generation and timeline meta data to schema entirely; Marker: Store cursor in markers of type :chunk_end --- lib/weaver.ex | 12 +- lib/weaver/absinthe/middleware/continue.ex | 41 +++- lib/weaver/absinthe/phase/document/result.ex | 108 ++++++++- lib/weaver/absinthe/schema.ex | 169 ++++++++++++-- lib/weaver/graph.ex | 40 +++- lib/weaver/resolvers.ex | 220 ------------------- lib/weaver/step.ex | 63 ++++-- lib/weaver/step/result.ex | 4 +- test/graph_test.exs | 18 +- test/nested_timelines_test.exs | 18 +- test/timelines_test.exs | 6 +- test/weaver_test.exs | 65 +++--- 12 files changed, 427 insertions(+), 337 deletions(-) delete mode 100644 lib/weaver/resolvers.ex diff --git a/lib/weaver.ex b/lib/weaver.ex index 2f2da8b..82a4f4a 100644 --- a/lib/weaver.ex +++ b/lib/weaver.ex @@ -24,7 +24,6 @@ defmodule Weaver do } def new(id) when is_binary(id), do: %__MODULE__{id: id} - def from(obj), do: new(Weaver.Node.id_for(obj)) end defmodule Marker do @@ -37,21 +36,22 @@ defmodule Weaver do data in `Weaver.Graph`. """ - @enforce_keys [:type, :ref, :val] - defstruct @enforce_keys + @enforce_keys [:type, :ref] + defstruct @enforce_keys ++ [:val, :cursor] @type t() :: %__MODULE__{ ref: any(), val: any(), + cursor: any(), type: :chunk_start | :chunk_end } - def chunk_start(id, val) do + def chunk_start(id, val \\ nil) do %__MODULE__{type: :chunk_start, ref: %Ref{id: id}, val: val} end - def chunk_end(id, val) do - %__MODULE__{type: :chunk_end, ref: %Ref{id: id}, val: val} + def chunk_end(id, val, cursor) do + %__MODULE__{type: :chunk_end, ref: %Ref{id: id}, val: val, cursor: cursor} end end diff --git a/lib/weaver/absinthe/middleware/continue.ex b/lib/weaver/absinthe/middleware/continue.ex index 1c354d6..390901d 100644 --- a/lib/weaver/absinthe/middleware/continue.ex +++ b/lib/weaver/absinthe/middleware/continue.ex @@ -25,8 +25,31 @@ defmodule Weaver.Absinthe.Middleware.Continue do # call resolver function only if this is the resolution part for the current step def call(%{state: :suspended, acc: %{resolution: path}, path: path} = res, fun) do + order = + res.definition.schema_node + |> Absinthe.Type.meta() + |> Map.take([:ordered_by, :order, :unique]) + + type_name = + res.definition.schema_node.type + |> Absinthe.Type.unwrap() + |> Absinthe.Type.expand(res.schema) + |> Map.get(:name) + + id_fun = + res.definition.schema_node.type + |> Absinthe.Type.unwrap() + |> Absinthe.Type.expand(res.schema) + |> id_fun_for() + + id_for = fn obj -> "#{type_name}:#{id_fun.(obj)}" end + cache = res.context.cache - parent_ref = res.source && Ref.from(res.source) + + parent_type_name = Map.get(res.parent_type, :name) + parent_id_fun = id_fun_for(res.parent_type) + + parent_ref = res.source && Ref.new("#{parent_type_name}:#{parent_id_fun.(res.source)}") [%Absinthe.Blueprint.Document.Field{name: field} | _] = path Map.get(res.acc, __MODULE__, %__MODULE__{}) @@ -40,9 +63,11 @@ defmodule Weaver.Absinthe.Middleware.Continue do |> Absinthe.Resolution.put_result({:ok, []}) step -> - resolved = fun.(step.prev_chunk_end) + prev_cursor = step.prev_chunk_end && step.prev_chunk_end.cursor + resolved = fun.(prev_cursor) - {value, meta, next} = Step.process_resolved(resolved, step, cache, parent_ref, field) + {value, meta, next} = + Step.process_resolved(resolved, step, cache, parent_ref, field, order, id_for) %{ res @@ -57,4 +82,14 @@ defmodule Weaver.Absinthe.Middleware.Continue do def call(res, _fun) do res end + + def id_fun_for(schema_type) do + schema_type + |> Absinthe.Type.meta(:weaver_id) + |> case do + nil -> &Weaver.Node.id_for/1 + fun when is_function(fun) -> fun + key -> &Map.get(&1, key) + end + end end diff --git a/lib/weaver/absinthe/phase/document/result.ex b/lib/weaver/absinthe/phase/document/result.ex index 61d75a9..e6b07ff 100644 --- a/lib/weaver/absinthe/phase/document/result.ex +++ b/lib/weaver/absinthe/phase/document/result.ex @@ -167,13 +167,109 @@ defmodule Weaver.Absinthe.Phase.Document.Result do defp next_path(_field, []), do: [] - defp to_ref(%{root_value: obj, fields: fields}) do - case fields do - [%{emitter: %{alias: "__weaver_id"}, value: id} | _] -> - %Ref{id: id} + defp to_ref(field) do + %Ref{id: "#{type_for(field)}:#{id_for(field)}"} + end + + defp id_for(%{emitter: %{schema_node: %{type: %{of_type: schema_type}}}} = field) do + id_for(put_in(field.emitter.schema_node.type, schema_type)) + end + + defp id_for(%{emitter: %{schema_node: %{type: schema_type}}, root_value: obj}) do + id_fun = + schema_type + |> get_concrete_type(obj, %{schema: schema_type.definition}) + |> Continue.id_fun_for() + + id_fun.(obj) + end + + defp type_for(%{emitter: %{schema_node: %{type: %{of_type: schema_type}}}} = field) do + type_for(put_in(field.emitter.schema_node.type, schema_type)) + end + + defp type_for(%{emitter: %{schema_node: %{type: schema_type}}, root_value: obj}) do + schema_type + |> get_concrete_type(obj, %{schema: schema_type.definition}) + |> Map.get(:name) + end + + defp get_concrete_type(%Type.Union{} = parent_type, source, res) do + # Type.Union.resolve_type(parent_type, source, res) + resolve_union_type(parent_type, source, res) + end + + defp get_concrete_type(%Type.Interface{} = parent_type, source, res) do + # Type.Interface.resolve_type(parent_type, source, res) + resolve_interface_type(parent_type, source, res) + end + + defp get_concrete_type(parent_type, _source, _res) do + parent_type + end + + def resolve_union_type(type, object, env, opts \\ [lookup: true]) + + def resolve_union_type(%{types: types} = union, obj, env = %{schema: schema}, opts) do + if resolver = Type.function(union, :resolve_type) do + case resolver.(obj, env) do + nil -> + nil - _ -> - Ref.from(obj) + ident when is_atom(ident) -> + if opts[:lookup] do + Absinthe.Schema.lookup_type(schema, ident) + else + ident + end + end + else + type_name = + Enum.find(types, fn + %{is_type_of: nil} -> + false + + type -> + type = Absinthe.Schema.lookup_type(schema, type) + Absinthe.Type.function(type, :is_type_of).(obj) + end) + + if opts[:lookup] do + Absinthe.Schema.lookup_type(schema, type_name) + else + type_name + end + end + end + + def resolve_interface_type(type, obj, env, opts \\ [lookup: true]) + + def resolve_interface_type(interface, obj, env = %{schema: schema}, opts) do + implementors = Absinthe.Schema.implementors(schema, interface.identifier) + + if resolver = Type.function(interface, :resolve_type) do + case resolver.(obj, env) do + nil -> + nil + + ident when is_atom(ident) -> + if opts[:lookup] do + Absinthe.Schema.lookup_type(schema, ident) + else + ident + end + end + else + type_name = + Enum.find(implementors, fn type -> + Absinthe.Type.function(type, :is_type_of).(obj) + end) + + if opts[:lookup] do + Absinthe.Schema.lookup_type(schema, type_name) + else + type_name + end end end end diff --git a/lib/weaver/absinthe/schema.ex b/lib/weaver/absinthe/schema.ex index e6b8e58..d43d561 100644 --- a/lib/weaver/absinthe/schema.ex +++ b/lib/weaver/absinthe/schema.ex @@ -5,14 +5,18 @@ defmodule Weaver.Absinthe.Schema do use Absinthe.Schema alias ExTwitter.Model.{Tweet, User} - alias Weaver.Resolvers + alias Weaver.Node - defimpl Weaver.Node, for: Tweet do - def id_for(tweet), do: "Tweet:#{tweet.id_str}" + @twitter_client Application.get_env(:weaver, :twitter)[:client_module] + @api_count Application.get_env(:weaver, :twitter)[:api_count] + @api_take Application.get_env(:weaver, :twitter)[:api_take] + + defimpl Node, for: Tweet do + def id_for(tweet), do: tweet.id_str end - defimpl Weaver.Node, for: User do - def id_for(user), do: "TwitterUser:#{user.screen_name}" + defimpl Node, for: User do + def id_for(user), do: user.screen_name end interface :node do @@ -22,17 +26,31 @@ defmodule Weaver.Absinthe.Schema do object :twitter_user do interface(:node) + meta(weaver_id: :screen_name) + is_type_of(fn %User{} -> true _other -> false end) - field(:id, non_null(:id), resolve: &weaver_id/3) + field(:id, non_null(:id)) field(:screen_name, non_null(:string)) field(:favorites_count, non_null(:integer), resolve: rsv(:favourites_count)) - field(:favorites, non_null(list_of(non_null(:tweet))), resolve: dispatched("favorites")) - field(:tweets, non_null(list_of(non_null(:tweet))), resolve: dispatched("tweets")) - field(:retweets, non_null(list_of(non_null(:tweet))), resolve: dispatched("retweets")) + + field :favorites, non_null(list_of(non_null(:tweet))) do + resolve(dispatched("favorites")) + meta(ordered_by: :id, order: :desc, unique: true) + end + + field :tweets, non_null(list_of(non_null(:tweet))) do + resolve(dispatched("tweets")) + meta(ordered_by: :id, order: :desc, unique: true) + end + + field :retweets, non_null(list_of(non_null(:tweet))) do + resolve(dispatched("retweets")) + meta(ordered_by: :id, order: :desc, unique: true) + end end object :tweet do @@ -43,7 +61,7 @@ defmodule Weaver.Absinthe.Schema do _other -> false end) - field(:id, non_null(:id), resolve: &weaver_id/3) + field(:id, non_null(:id)) field(:text, non_null(:string), resolve: rsv(:full_text)) field(:published_at, non_null(:string), resolve: rsv(:created_at)) field(:likes_count, non_null(:integer), resolve: rsv(:favorite_count)) @@ -51,8 +69,17 @@ defmodule Weaver.Absinthe.Schema do field(:user, non_null(:twitter_user)) field(:likes, non_null(list_of(non_null(:twitter_like))), resolve: dispatched("likes")) field(:mentions, non_null(list_of(non_null(:twitter_user))), resolve: dispatched("mentions")) - field(:retweets, non_null(list_of(non_null(:tweet))), resolve: dispatched("retweets")) - field(:replies, non_null(list_of(non_null(:tweet))), resolve: dispatched("replies")) + + field :retweets, non_null(list_of(non_null(:tweet))) do + resolve(dispatched("retweets")) + meta(ordered_by: :id, order: :desc, unique: true) + end + + field :replies, non_null(list_of(non_null(:tweet))) do + resolve(dispatched("replies")) + meta(ordered_by: :id, order: :desc, unique: true) + end + field(:retweet_of, :tweet, resolve: rsv(:retweeted_status)) end @@ -68,8 +95,8 @@ defmodule Weaver.Absinthe.Schema do field :node, non_null(:node) do arg(:id, :string) - resolve(fn _, %{id: id}, _ -> - obj = Resolvers.retrieve_by_id(id) + resolve(fn _, %{id: "TwitterUser:" <> id}, _ -> + obj = @twitter_client.user(id) {:ok, obj} end) end @@ -82,8 +109,8 @@ defmodule Weaver.Absinthe.Schema do defp dispatched(field) do fn obj, _, _ -> - fun = fn prev_end_marker -> - Resolvers.dispatched(obj, field, prev_end_marker) + fun = fn prev_min_id -> + dispatched(obj, field, prev_min_id) end {:middleware, Weaver.Absinthe.Middleware.Dispatch, fun} @@ -94,7 +121,113 @@ defmodule Weaver.Absinthe.Schema do fn obj, _, _ -> Map.fetch(obj, field) end end - defp weaver_id(obj, _, _) do - {:ok, Weaver.Node.id_for(obj)} + def dispatched(obj = %User{}, "favorites", prev_min_id) do + case prev_min_id do + nil -> + @twitter_client.favorites(id: obj.id, tweet_mode: :extended, count: @api_count) + + min_id -> + @twitter_client.favorites( + id: obj.id, + tweet_mode: :extended, + count: @api_count, + max_id: min_id - 1 + ) + end + |> case do + [] -> {:done, []} + tweets -> {:continue, Enum.take(tweets, @api_take), cursor(tweets)} + end + end + + def dispatched(obj = %User{}, "tweets", prev_min_id) do + case prev_min_id do + nil -> + @twitter_client.user_timeline( + screen_name: obj.screen_name, + include_rts: false, + tweet_mode: :extended, + count: @api_count + ) + + min_id -> + @twitter_client.user_timeline( + screen_name: obj.screen_name, + include_rts: false, + tweet_mode: :extended, + count: @api_count, + max_id: min_id - 1 + ) + end + |> case do + [] -> {:done, []} + tweets -> {:continue, Enum.take(tweets, @api_take), cursor(tweets)} + end + end + + def dispatched(obj = %User{}, "retweets", prev_min_id) do + case prev_min_id do + nil -> + @twitter_client.user_timeline( + screen_name: obj.screen_name, + tweet_mode: :extended, + count: @api_count + ) + + min_id -> + @twitter_client.user_timeline( + screen_name: obj.screen_name, + tweet_mode: :extended, + count: @api_count, + max_id: min_id - 1 + ) + end + |> Enum.filter(& &1.retweeted_status) + |> case do + [] -> {:done, []} + tweets -> {:continue, Enum.take(tweets, @api_take), cursor(tweets)} + end + end + + def dispatched(%Tweet{}, "likes", _prev_min_id) do + {:done, []} + end + + def dispatched(%Tweet{}, "replies", _prev_min_id) do + {:done, []} + end + + def dispatched(obj = %Tweet{}, "retweets", prev_min_id) do + case prev_min_id do + nil -> + @twitter_client.retweets(obj.id, count: @api_count, tweet_mode: :extended) + + min_id -> + @twitter_client.retweets(obj.id, + count: @api_count, + tweet_mode: :extended, + max_id: min_id - 1 + ) + end + |> case do + [] -> {:done, []} + tweets -> {:continue, Enum.take(tweets, @api_take), cursor(tweets)} + end + end + + def dispatched(obj = %Tweet{}, "mentions", _prev_min_id) do + users = + case obj.entities.user_mentions do + [] -> [] + mentions -> mentions |> Enum.map(& &1.id) |> @twitter_client.user_lookup() + end + + {:done, users} + end + + def cursor(tweets) do + tweets + |> List.last() + |> Map.get(:id) end end diff --git a/lib/weaver/graph.ex b/lib/weaver/graph.ex index 0c211f7..3167f75 100644 --- a/lib/weaver/graph.ex +++ b/lib/weaver/graph.ex @@ -144,11 +144,12 @@ defmodule Weaver.Graph do [ "#{sub_var} #{marker_var} .", - "#{marker_var} #{inspect(predicate)} .", + "#{marker_var} #{property(predicate)} .", "#{marker_var} #{val_var} .", "#{marker_var} #{ref_var} .", - ~s|#{marker_var} "#{type_str}" .| + "#{marker_var} #{property(type_str)} ." ] + |> maybe_add_nquad(marker_var, "weaver.markers.cursor", marker.cursor) {subject, predicate, object} -> sub = property(subject, varnames) @@ -184,11 +185,12 @@ defmodule Weaver.Graph do [ "#{sub_var} #{marker_var} .", - "#{marker_var} #{inspect(predicate)} .", + "#{marker_var} #{property(predicate)} .", "#{marker_var} #{val_var} .", "#{marker_var} #{ref_var} .", - ~s|#{marker_var} "#{type_str}" .| + "#{marker_var} #{property(type_str)} ." ] + |> maybe_add_nquad(marker_var, "weaver.markers.cursor", marker.cursor) _other -> [] @@ -197,14 +199,14 @@ defmodule Weaver.Graph do id_statements = Enum.flat_map(varnames, fn {%Ref{id: id}, varname} -> - ["uid(#{varname}) #{inspect(id)} ."] + ["uid(#{varname}) #{property(id)} ."] {%Marker{}, _varname} -> [] end) result = - [delete: del_statements, set: id_statements ++ add_statements] + [delete: del_statements, set: add_statements ++ id_statements] |> Enum.flat_map(fn {_op, []} -> [] {op, list} -> [%{op => Enum.join(list, "\n")}] @@ -236,11 +238,12 @@ defmodule Weaver.Graph do query = ~s""" { - markers(func: eq(id, #{inspect(ref.id)})) { + markers(func: eq(id, #{property(ref.id)})) { weaver.markers @filter(#{filters_str}) (orderdesc: weaver.markers.intValue, orderdesc: weaver.markers.type#{ limit_str }) { weaver.markers.intValue + weaver.markers.cursor weaver.markers.object { id } weaver.markers.type } @@ -260,11 +263,12 @@ defmodule Weaver.Graph do "weaver.markers.intValue" => val, "weaver.markers.type" => type_str, "weaver.markers.object" => %{"id" => id} - } -> + } = payload -> %Marker{ type: to_marker_type(type_str), val: val, - ref: %Ref{id: id} + ref: %Ref{id: id}, + cursor: payload["weaver.markers.cursor"] } end) @@ -281,7 +285,7 @@ defmodule Weaver.Graph do result = ~s""" { - countRelation(func: eq(id, #{inspect(id)})) { + countRelation(func: eq(id, #{property(id)})) { c : count(#{relation}) } } @@ -398,11 +402,23 @@ defmodule Weaver.Graph do end end - defp property(int, _varnames) when is_integer(int) do + defp property(other, _varnames) do + property(other) + end + + defp property(int) when is_integer(int) do ~s|"#{int}"^^| end - defp property(other, _varnames), do: inspect(other) + defp property(other), do: inspect(other) + + defp maybe_add_nquad(list, _sub, _pred, nil) do + list + end + + defp maybe_add_nquad(list, sub, pred, obj) do + ["#{sub} <#{pred}> #{property(obj)} ." | list] + end @doc """ Generates a variable name from a number. diff --git a/lib/weaver/resolvers.ex b/lib/weaver/resolvers.ex deleted file mode 100644 index c8b2394..0000000 --- a/lib/weaver/resolvers.ex +++ /dev/null @@ -1,220 +0,0 @@ -defmodule Weaver.Resolvers do - @moduledoc """ - Weaver-style GraphQL resolvers used in tests. - - Resolvers can return `:dispatch` to declare that the resolver should - run in another, dispatched, step to resolve this predicate/edge. It will be - returned as dispatched step and should be handled by the caller. - """ - - alias Weaver.{Marker, Ref} - alias ExTwitter.Model.{Tweet, User} - - @twitter_client Application.get_env(:weaver, :twitter)[:client_module] - @api_count Application.get_env(:weaver, :twitter)[:api_count] - @api_take Application.get_env(:weaver, :twitter)[:api_take] - - def retrieve_by_id("TwitterUser:" <> id) do - @twitter_client.user(id) - end - - def end_marker(objs) when is_list(objs) do - objs - |> Enum.min_by(& &1.id) - |> marker(:chunk_end) - end - - def start_marker(objs) when is_list(objs) do - objs - |> Enum.max_by(& &1.id) - |> marker(:chunk_start) - end - - def marker_val(%{id: val}), do: val - - def marker(obj, type) do - %Marker{type: type, ref: Ref.from(obj), val: marker_val(obj)} - end - - def resolve_leaf(obj = %User{}, "screenName") do - obj.screen_name - end - - def resolve_leaf(obj = %User{}, "favoritesCount") do - obj.favourites_count - end - - def resolve_leaf(obj = %Tweet{}, "text") do - obj.full_text - end - - def resolve_leaf(obj = %Tweet{}, "publishedAt") do - obj.created_at - end - - def resolve_leaf(obj = %Tweet{}, "likesCount") do - obj.favorite_count - end - - def resolve_leaf(obj = %Tweet{}, "retweetsCount") do - obj.retweet_count - end - - def resolve_node(%User{}, "favorites"), do: :dispatch - def resolve_node(%User{}, "tweets"), do: :dispatch - def resolve_node(%User{}, "retweets"), do: :dispatch - - def resolve_node(obj = %Tweet{}, "user") do - obj.user - end - - def resolve_node(obj = %Tweet{}, "retweetOf") do - obj.retweeted_status - end - - def resolve_node(%Tweet{}, "likes"), do: :dispatch - def resolve_node(%Tweet{}, "replies"), do: :dispatch - def resolve_node(%Tweet{}, "retweets"), do: :dispatch - def resolve_node(%Tweet{}, "mentions"), do: :dispatch - - def total_count(obj = %User{}, "favorites") do - obj.favourites_count - end - - def total_count(obj = %Tweet{}, "likesCount") do - obj.favorite_count - end - - def total_count(obj = %Tweet{}, "retweetsCount") do - obj.retweet_count - end - - def total_count(_obj, _relation), do: nil - - def dispatched(obj = %User{}, "favorites", prev_end_marker) do - tweets = - case prev_end_marker do - nil -> - @twitter_client.favorites(id: obj.id, tweet_mode: :extended, count: @api_count) - - %Marker{val: min_id} -> - @twitter_client.favorites( - id: obj.id, - tweet_mode: :extended, - count: @api_count, - max_id: min_id - 1 - ) - end - - case tweets do - [] -> - {:done, []} - - tweets -> - {:continue, Enum.take(tweets, @api_take), end_marker(tweets)} - end - end - - def dispatched(obj = %User{}, "tweets", prev_end_marker) do - tweets = - case prev_end_marker do - nil -> - @twitter_client.user_timeline( - screen_name: obj.screen_name, - include_rts: false, - tweet_mode: :extended, - count: @api_count - ) - - %Marker{val: min_id} -> - @twitter_client.user_timeline( - screen_name: obj.screen_name, - include_rts: false, - tweet_mode: :extended, - count: @api_count, - max_id: min_id - 1 - ) - end - - case tweets do - [] -> - {:done, []} - - tweets -> - {:continue, Enum.take(tweets, @api_take), end_marker(tweets)} - end - end - - def dispatched(obj = %User{}, "retweets", prev_end_marker) do - tweets = - case prev_end_marker do - nil -> - @twitter_client.user_timeline( - screen_name: obj.screen_name, - tweet_mode: :extended, - count: @api_count - ) - - %Marker{val: min_id} -> - @twitter_client.user_timeline( - screen_name: obj.screen_name, - tweet_mode: :extended, - count: @api_count, - max_id: min_id - 1 - ) - end - - case Enum.filter(tweets, & &1.retweeted_status) do - [] -> - {:done, []} - - tweets -> - tweets = - tweets - |> Enum.take(@api_take) - - {:continue, tweets, end_marker(tweets)} - end - end - - def dispatched(%Tweet{}, "likes", _prev_end_marker) do - {:done, []} - end - - def dispatched(%Tweet{}, "replies", _prev_end_marker) do - {:done, []} - end - - def dispatched(obj = %Tweet{}, "retweets", prev_end_marker) do - tweets = - case prev_end_marker do - nil -> - @twitter_client.retweets(obj.id, count: @api_count, tweet_mode: :extended) - - %Marker{val: min_id} -> - @twitter_client.retweets(obj.id, - count: @api_count, - tweet_mode: :extended, - max_id: min_id - 1 - ) - end - - case tweets do - [] -> - {:done, []} - - tweets -> - {:continue, Enum.take(tweets, @api_take), end_marker(tweets)} - end - end - - def dispatched(obj = %Tweet{}, "mentions", _prev_end_marker) do - users = - case obj.entities.user_mentions do - [] -> [] - mentions -> mentions |> Enum.map(& &1.id) |> @twitter_client.user_lookup() - end - - {:done, users} - end -end diff --git a/lib/weaver/step.ex b/lib/weaver/step.ex index 6bc909a..1a1bcc6 100644 --- a/lib/weaver/step.ex +++ b/lib/weaver/step.ex @@ -2,10 +2,10 @@ defmodule Weaver.Step do @moduledoc """ Core processing logic for each chunk of streamed data. """ - alias Weaver.{Marker, Resolvers} + alias Weaver.Marker - def process_resolved(resolved, step, cache, parent_ref, field) do - case analyze_resolved(resolved, step) do + def process_resolved(resolved, step, cache, parent_ref, field, order, id_for) do + case analyze_resolved(resolved, step, order, id_for) do {:entire_data, []} -> meta = meta_delete_all(cache, parent_ref, field) @@ -13,7 +13,7 @@ defmodule Weaver.Step do {:entire_data, objs} -> meta = - [{:add, parent_ref, field, Resolvers.start_marker(objs)}] ++ + [{:add, parent_ref, field, start_marker(objs, order, id_for)}] ++ meta_delete_all(cache, parent_ref, field) {objs, meta, nil} @@ -26,9 +26,11 @@ defmodule Weaver.Step do {objs, meta, nil} # no gap or gap not closed -> continue with this marker - {:continue, objs, new_chunk_end} -> + {:continue, objs, cursor} -> + new_chunk_end = end_marker(objs, order, id_for, cursor) + meta = [ - first_meta(step, resolved, parent_ref, field), + first_meta(step, resolved, parent_ref, field, order, id_for), {:add, parent_ref, field, new_chunk_end} ] @@ -39,7 +41,7 @@ defmodule Weaver.Step do # gap closed -> look up the next chunk start in next iteration {:gap_closed, objs} -> meta = [ - first_meta(step, resolved, parent_ref, field), + first_meta(step, resolved, parent_ref, field, order, id_for), {:del, parent_ref, field, step.next_chunk_start} ] @@ -55,12 +57,19 @@ defmodule Weaver.Step do end end - defp first_meta(step = %{prev_chunk_end: %Marker{}}, _resolved, parent_ref, field) do + defp first_meta( + step = %{prev_chunk_end: %Marker{}}, + _resolved, + parent_ref, + field, + _order, + _id_for + ) do {:del, parent_ref, field, step.prev_chunk_end} end - defp first_meta(_step, {:continue, objs, _marker}, parent_ref, field) do - {:add, parent_ref, field, Resolvers.start_marker(objs)} + defp first_meta(_step, {:continue, objs, _marker}, parent_ref, field, order, id_for) do + {:add, parent_ref, field, start_marker(objs, order, id_for)} end defp meta_delete_all(cache, parent_ref, field, opts \\ []) do @@ -69,30 +78,44 @@ defmodule Weaver.Step do |> Enum.map(&{:del, parent_ref, field, &1}) end - defp analyze_resolved({:done, objs}, %{prev_chunk_end: %Marker{}}) do + defp analyze_resolved({:done, objs}, %{prev_chunk_end: %Marker{}}, _order, _id_for) do {:last_data, objs} end - defp analyze_resolved({:done, objs}, _) do + defp analyze_resolved({:done, objs}, _, _order, _id_for) do {:entire_data, objs} end # no gap - defp analyze_resolved({:continue, objs, new_chunk_end}, %{next_chunk_start: nil}) do - {:continue, objs, new_chunk_end} + defp analyze_resolved({:continue, objs, cursor}, %{next_chunk_start: nil}, _order, _id_for) do + {:continue, objs, cursor} end # gap closed? - defp analyze_resolved({:continue, objs, new_chunk_end}, step = %{}) do - case Enum.split_while(objs, &before_marker?(&1, step.next_chunk_start)) do - {objs, []} -> {:continue, objs, new_chunk_end} + defp analyze_resolved({:continue, objs, cursor}, step = %{}, order, id_for) do + case Enum.split_while(objs, &before_marker?(&1, step.next_chunk_start, order, id_for)) do + {objs, []} -> {:continue, objs, cursor} {objs, __} -> {:gap_closed, objs} end end - defp before_marker?(obj, marker) do - Resolvers.marker_val(obj) > marker.val && - Weaver.Node.id_for(obj) != marker.ref.id + defp before_marker?(obj, marker, %{ordered_by: order_field}, id_for) do + Map.get(obj, order_field) > marker.val && + id_for.(obj) != marker.ref.id + end + + def start_marker(objs, %{ordered_by: order_field}, id_for) do + obj = List.first(objs) + id = id_for.(obj) + val = Map.get(obj, order_field) + Marker.chunk_start(id, val) + end + + def end_marker(objs, %{ordered_by: order_field}, id_for, cursor) do + obj = List.last(objs) + id = id_for.(obj) + val = Map.get(obj, order_field) + Marker.chunk_end(id, val, cursor) end def load_markers(step = %{next_chunk_start: val}, _opts, _cache, _parent_ref, _field) diff --git a/lib/weaver/step/result.ex b/lib/weaver/step/result.ex index 23cc59d..cab3e2e 100644 --- a/lib/weaver/step/result.ex +++ b/lib/weaver/step/result.ex @@ -32,9 +32,9 @@ defmodule Weaver.Step.Result do {[tuple | data], meta, errors, dispatched, next} end - def add_relation_data(result, {from = %Ref{}, predicate, [obj | objs]}) do + def add_relation_data(result, {from = %Ref{}, predicate, [obj_ref = %Ref{} | objs]}) do result - |> add_data({from, predicate, Ref.from(obj)}) + |> add_data({from, predicate, obj_ref}) |> add_relation_data({from, predicate, objs}) end diff --git a/test/graph_test.exs b/test/graph_test.exs index c79c654..509404c 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -24,11 +24,11 @@ defmodule Weaver.GraphTest do meta = [ {:add, user1, "favorites", Marker.chunk_start("Tweet:140", 140)}, - {:add, user1, "favorites", Marker.chunk_end("Tweet:134", 134)}, + {:add, user1, "favorites", Marker.chunk_end("Tweet:134", 134, 134)}, {:add, user1, "favorites", Marker.chunk_start("Tweet:34", 34)}, - {:add, user1, "favorites", Marker.chunk_end("Tweet:34", 34)}, + {:add, user1, "favorites", Marker.chunk_end("Tweet:34", 34, 34)}, {:add, user1, "favorites", Marker.chunk_start("Tweet:4", 4)}, - {:add, user1, "favorites", Marker.chunk_end("Tweet:3", 3)}, + {:add, user1, "favorites", Marker.chunk_end("Tweet:3", 3, 3)}, {:add, user1, "favorites", Marker.chunk_start("Tweet:1", 1)} ] @@ -52,25 +52,25 @@ defmodule Weaver.GraphTest do test "markers", %{user1: user1} do assert {:ok, [marker1, marker2]} = markers(user1, "favorites", limit: 2) assert marker1 == Marker.chunk_start("Tweet:140", 140) - assert marker2 == Marker.chunk_end("Tweet:134", 134) + assert marker2 == Marker.chunk_end("Tweet:134", 134, 134) end test "delete markers", %{user1: user1} do assert %{} = store!([], [{:del, user1, "favorites", Marker.chunk_start("Tweet:140", 140)}]) assert {:ok, [marker2]} = markers(user1, "favorites", limit: 1) - assert marker2 == Marker.chunk_end("Tweet:134", 134) + assert marker2 == Marker.chunk_end("Tweet:134", 134, 134) end test "delete and add same markers", %{user1: user1} do assert %{} = store!([], [ - {:add, user1, "favorites", Marker.chunk_end("Tweet:140", 140)}, + {:add, user1, "favorites", Marker.chunk_end("Tweet:140", 140, 140)}, {:del, user1, "favorites", Marker.chunk_start("Tweet:140", 140)} ]) assert {:ok, [marker2]} = markers(user1, "favorites", limit: 1) - assert marker2 == Marker.chunk_end("Tweet:140", 140) + assert marker2 == Marker.chunk_end("Tweet:140", 140, 140) end test "markers less_than", %{user1: user1} do @@ -78,9 +78,9 @@ defmodule Weaver.GraphTest do assert markers == [ Marker.chunk_start("Tweet:34", 34), - Marker.chunk_end("Tweet:34", 34), + Marker.chunk_end("Tweet:34", 34, 34), Marker.chunk_start("Tweet:4", 4), - Marker.chunk_end("Tweet:3", 3), + Marker.chunk_end("Tweet:3", 3, 3), Marker.chunk_start("Tweet:1", 1) ] end diff --git a/test/nested_timelines_test.exs b/test/nested_timelines_test.exs index 70fcbbe..d4e33ec 100644 --- a/test/nested_timelines_test.exs +++ b/test/nested_timelines_test.exs @@ -57,7 +57,7 @@ defmodule Weaver.NestedTimelinesTest do ]) |> assert_meta([ {:add, user_ref, "favorites", Marker.chunk_start("Tweet:11", 11)}, - {:add, user_ref, "favorites", Marker.chunk_end("Tweet:10", 10)} + {:add, user_ref, "favorites", Marker.chunk_end("Tweet:10", 10, 10)} ]) |> assert_dispatched_paths([]) |> assert_next_path([%{name: "favorites"}, %{name: "node"}, %{name: nil}]) @@ -71,8 +71,8 @@ defmodule Weaver.NestedTimelinesTest do {user_ref, "favorites", %Ref{id: "Tweet:9"}} ]) |> assert_meta([ - {:del, user_ref, "favorites", Marker.chunk_end("Tweet:10", 10)}, - {:add, user_ref, "favorites", Marker.chunk_end("Tweet:8", 8)} + {:del, user_ref, "favorites", Marker.chunk_end("Tweet:10", 10, 10)}, + {:add, user_ref, "favorites", Marker.chunk_end("Tweet:8", 8, 8)} ]) |> assert_dispatched_paths([]) |> assert_next_path([%{name: "favorites"}, %{name: "node"}, %{name: nil}]) @@ -81,7 +81,7 @@ defmodule Weaver.NestedTimelinesTest do |> weave_next(Twitter, :favorites, twitter_mock_for(user, favorites, max_id: 7)) |> assert_data([]) |> assert_meta([ - {:del, user_ref, "favorites", Marker.chunk_end("Tweet:8", 8)} + {:del, user_ref, "favorites", Marker.chunk_end("Tweet:8", 8, 8)} ]) |> assert_dispatched_paths([]) |> refute_next() @@ -101,7 +101,7 @@ defmodule Weaver.NestedTimelinesTest do ]) |> assert_meta([ {:add, user_ref, "tweets", Marker.chunk_start("Tweet:#{tweet1.id}", tweet1.id)}, - {:add, user_ref, "tweets", Marker.chunk_end("Tweet:#{tweet2.id}", tweet2.id)} + {:add, user_ref, "tweets", Marker.chunk_end("Tweet:#{tweet2.id}", tweet2.id, tweet2.id)} ]) |> assert_dispatched_paths([ [%{name: "retweets"} | _], @@ -124,7 +124,7 @@ defmodule Weaver.NestedTimelinesTest do {:add, %Ref{id: "Tweet:#{tweet1.id}"}, "retweets", Marker.chunk_start("Tweet:#{retweet1.id}", retweet1.id)}, {:add, %Ref{id: "Tweet:#{tweet1.id}"}, "retweets", - Marker.chunk_end("Tweet:#{retweet1.id}", retweet1.id)} + Marker.chunk_end("Tweet:#{retweet1.id}", retweet1.id, retweet1.id)} ]) |> assert_dispatched_paths([]) |> assert_next_path([%{name: "retweets"}, 0, %{name: "tweets"}, %{name: "node"}, %{name: nil}]) @@ -134,7 +134,7 @@ defmodule Weaver.NestedTimelinesTest do |> assert_data([]) |> assert_meta([ {:del, %Ref{id: "Tweet:#{tweet1.id}"}, "retweets", - Marker.chunk_end("Tweet:#{retweet1.id}", retweet1.id)} + Marker.chunk_end("Tweet:#{retweet1.id}", retweet1.id, retweet1.id)} ]) |> assert_dispatched_paths([]) |> refute_next() @@ -153,7 +153,7 @@ defmodule Weaver.NestedTimelinesTest do {:add, %Ref{id: "Tweet:#{tweet2.id}"}, "retweets", Marker.chunk_start("Tweet:#{retweet2.id}", retweet2.id)}, {:add, %Ref{id: "Tweet:#{tweet2.id}"}, "retweets", - Marker.chunk_end("Tweet:#{retweet2.id}", retweet2.id)} + Marker.chunk_end("Tweet:#{retweet2.id}", retweet2.id, retweet2.id)} ]) |> assert_dispatched_paths([]) |> assert_next_path([%{name: "retweets"}, 1, %{name: "tweets"}, %{name: "node"}, %{name: nil}]) @@ -163,7 +163,7 @@ defmodule Weaver.NestedTimelinesTest do |> assert_data([]) |> assert_meta([ {:del, %Ref{id: "Tweet:#{tweet2.id}"}, "retweets", - Marker.chunk_end("Tweet:#{retweet2.id}", retweet2.id)} + Marker.chunk_end("Tweet:#{retweet2.id}", retweet2.id, retweet2.id)} ]) |> assert_dispatched_paths([]) |> refute_next() diff --git a/test/timelines_test.exs b/test/timelines_test.exs index 27659bf..766e5d3 100644 --- a/test/timelines_test.exs +++ b/test/timelines_test.exs @@ -43,16 +43,16 @@ defmodule Weaver.TimelinesTest do |> assert_data([ {%Ref{id: "Tweet:#{tweet2.id}"}, "text", tweet2.full_text}, {%Ref{id: "Tweet:#{tweet2.id}"}, "__weaver_type", "Tweet"}, - {%Ref{id: "Tweet:#{tweet2.id}"}, "__weaver_id", "Tweet:#{tweet2.id}"}, + {%Ref{id: "Tweet:#{tweet2.id}"}, "__weaver_id", "#{tweet2.id}"}, {user_ref, "tweets", %Ref{id: "Tweet:#{tweet2.id}"}}, {%Ref{id: "Tweet:#{tweet1.id}"}, "text", tweet1.full_text}, {%Ref{id: "Tweet:#{tweet1.id}"}, "__weaver_type", "Tweet"}, - {%Ref{id: "Tweet:#{tweet1.id}"}, "__weaver_id", "Tweet:#{tweet1.id}"}, + {%Ref{id: "Tweet:#{tweet1.id}"}, "__weaver_id", "#{tweet1.id}"}, {user_ref, "tweets", %Ref{id: "Tweet:#{tweet1.id}"}} ]) |> assert_meta([ {:add, user_ref, "tweets", Marker.chunk_start("Tweet:#{tweet1.id}", tweet1.id)}, - {:add, user_ref, "tweets", Marker.chunk_end("Tweet:#{tweet2.id}", tweet2.id)} + {:add, user_ref, "tweets", Marker.chunk_end("Tweet:#{tweet2.id}", tweet2.id, tweet2.id)} ]) |> assert_dispatched_paths([]) |> assert_next_path([%{name: "tweets"}, %{name: "node"}, %{name: nil}]) diff --git a/test/weaver_test.exs b/test/weaver_test.exs index 11924d0..3d110cf 100644 --- a/test/weaver_test.exs +++ b/test/weaver_test.exs @@ -232,7 +232,7 @@ defmodule WeaverTest do {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:11", 11)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:10", 10)} + Marker.chunk_end("Tweet:10", 10, 10)} ]) |> assert_next_path([%{name: "favorites"} | _]) |> assert_next_state(%{ @@ -254,12 +254,13 @@ defmodule WeaverTest do ]) |> assert_meta([ {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:10", 10)}, - {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_end("Tweet:8", 8)} + Marker.chunk_end("Tweet:10", 10, 10)}, + {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", + Marker.chunk_end("Tweet:8", 8, 8)} ]) |> assert_next_path([%{name: "favorites"} | _]) |> assert_next_state(%{ - prev_chunk_end: %Marker{type: :chunk_end, ref: %Ref{id: "Tweet:8"}, val: 8} + prev_chunk_end: %Marker{type: :chunk_end, ref: %Ref{id: "Tweet:8"}, val: 8, cursor: 8} }) |> assert_dispatched_paths([ [%{name: "retweets"} | _], @@ -279,14 +280,15 @@ defmodule WeaverTest do {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:20", 20)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:16", 16)}, + Marker.chunk_end("Tweet:16", 16, 16)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:12", 12)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:12", 12)}, + Marker.chunk_end("Tweet:12", 12, 12)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:10", 10)}, - {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_end("Tweet:8", 8)}, + {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", + Marker.chunk_end("Tweet:8", 8, 8)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:7", 7)} ] @@ -342,9 +344,9 @@ defmodule WeaverTest do ]) |> assert_meta([ {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:16", 16)}, + Marker.chunk_end("Tweet:16", 16, 16)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:14", 14)} + Marker.chunk_end("Tweet:14", 14, 14)} ]) |> assert_next_path([%{name: "favorites"} | _]) |> assert_next_state(%{ @@ -363,7 +365,7 @@ defmodule WeaverTest do ]) |> assert_meta([ {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:14", 14)}, + Marker.chunk_end("Tweet:14", 14, 14)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:12", 12)} ]) @@ -384,7 +386,7 @@ defmodule WeaverTest do ]) |> assert_meta([ {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:12", 12)}, + Marker.chunk_end("Tweet:12", 12, 12)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:10", 10)} ]) @@ -402,7 +404,8 @@ defmodule WeaverTest do |> assert_data([]) |> assert_dispatched_paths([]) |> assert_meta([ - {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_end("Tweet:8", 8)}, + {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", + Marker.chunk_end("Tweet:8", 8, 8)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:7", 7)} ]) @@ -429,7 +432,7 @@ defmodule WeaverTest do {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:20", 20)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:20", 20)}, + Marker.chunk_end("Tweet:20", 20, 20)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:16", 16)} ] @@ -480,14 +483,15 @@ defmodule WeaverTest do {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:20", 20)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:16", 16)}, + Marker.chunk_end("Tweet:16", 16, 16)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:12", 12)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:12", 12)}, + Marker.chunk_end("Tweet:12", 12, 12)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:10", 10)}, - {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_end("Tweet:8", 8)}, + {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", + Marker.chunk_end("Tweet:8", 8, 8)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:7", 7)} ] @@ -555,7 +559,7 @@ defmodule WeaverTest do {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:21", 21)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:21", 21)} + Marker.chunk_end("Tweet:21", 21, 21)} ]) |> weave_next( Twitter, @@ -564,18 +568,19 @@ defmodule WeaverTest do ) |> assert_meta([ {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:21", 21)}, + Marker.chunk_end("Tweet:21", 21, 21)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:20", 20)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:16", 16)}, + Marker.chunk_end("Tweet:16", 16, 16)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:12", 12)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:12", 12)}, + Marker.chunk_end("Tweet:12", 12, 12)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:10", 10)}, - {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_end("Tweet:8", 8)}, + {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", + Marker.chunk_end("Tweet:8", 8, 8)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:7", 7)} ]) @@ -605,23 +610,24 @@ defmodule WeaverTest do ]) |> assert_meta([ {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:16", 16)}, + Marker.chunk_end("Tweet:16", 16, 16)}, {:add, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:15", 15)} + Marker.chunk_end("Tweet:15", 15, 15)} ]) # favorites last pt. |> weave_next(Twitter, :favorites, twitter_mock_for(user, favorites, max_id: 14)) |> assert_meta([ {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:15", 15)}, + Marker.chunk_end("Tweet:15", 15, 15)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:12", 12)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:12", 12)}, + Marker.chunk_end("Tweet:12", 12, 12)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:10", 10)}, - {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_end("Tweet:8", 8)}, + {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", + Marker.chunk_end("Tweet:8", 8, 8)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:7", 7)} ]) @@ -640,14 +646,15 @@ defmodule WeaverTest do {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:20", 20)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:16", 16)}, + Marker.chunk_end("Tweet:16", 16, 16)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:12", 12)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", - Marker.chunk_end("Tweet:12", 12)}, + Marker.chunk_end("Tweet:12", 12, 12)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:10", 10)}, - {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_end("Tweet:8", 8)}, + {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", + Marker.chunk_end("Tweet:8", 8, 8)}, {:del, %Ref{id: "TwitterUser:elixirdigest"}, "favorites", Marker.chunk_start("Tweet:7", 7)} ])