Skip to content
6 changes: 4 additions & 2 deletions lib/contentful/entry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ defmodule Contentful.Entry do
"""

alias Contentful.SysData
alias Contentful.Metadata

defstruct [:sys, fields: []]
defstruct [:sys, fields: [], metadata: %Metadata{}]

@type t :: %Contentful.Entry{
fields: list(),
sys: SysData.t()
sys: SysData.t(),
metadata: Metadata.t()
}
end
81 changes: 65 additions & 16 deletions lib/contentful/entry/link_resolver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,61 @@ defmodule Contentful.Entry.LinkResolver do
Inspired by https://github.com/contentful/contentful.js/blob/master/ADVANCED.md#link-resolution
"""
@spec replace_links_with_entities(Entry.t(), map()) :: Entry.t()
def replace_links_with_entities(%Entry{fields: fields} = entry, %{} = includes) do
def replace_links_with_entities(%Entry{} = entry, %{} = includes) do
replace_links_with_entities(entry, includes, MapSet.new())
end

def replace_links_with_entities(entity, _includes), do: entity

defp replace_links_with_entities(%Entry{sys: %{id: entry_id}} = entry, %{} = includes, visited) do
if MapSet.member?(visited, entry_id) do
# Return the entry without processing links to break the cycle
entry
else
updated_visited = MapSet.put(visited, entry_id)

updated_fields =
entry.fields
|> Enum.reduce(%{}, fn {name, value}, fields_with_links_resolved ->
new_value =
resolve_links_in_field_with_nesting(value, includes, entry_id, updated_visited)

Map.put(fields_with_links_resolved, name, new_value)
end)

struct(entry, fields: updated_fields)
end
end

defp replace_links_with_entities(%Entry{} = entry, %{} = includes, visited) do
updated_fields =
fields
entry.fields
|> Enum.reduce(%{}, fn {name, value}, fields_with_links_resolved ->
new_value = resolve_links_in_field_with_nesting(value, includes)
new_value = resolve_links_in_field_with_nesting(value, includes, "unknown", visited)

Map.put(fields_with_links_resolved, name, new_value)
end)

struct(entry, fields: updated_fields)
end

def replace_links_with_entities(entity, _includes), do: entity

defp resolve_links_in_field_with_nesting(field_value, includes) do
case resolved = resolve_links_in_field(field_value, includes) do
%Entry{} ->
replace_links_with_entities(resolved, includes)
defp resolve_links_in_field_with_nesting(
field_value,
includes,
parent_entry_id \\ "unknown",
visited \\ MapSet.new()
) do
case resolved = resolve_links_in_field(field_value, includes, parent_entry_id, visited) do
%Entry{sys: %{id: linked_entry_id}} = resolved_entry ->
# Check for potential circular reference using the visited set
if MapSet.member?(visited, linked_entry_id) do
resolved_entry
else
replace_links_with_entities(resolved_entry, includes, visited)
end

%Entry{} = resolved_entry ->
replace_links_with_entities(resolved_entry, includes, visited)

_ ->
resolved
Expand All @@ -45,7 +82,9 @@ defmodule Contentful.Entry.LinkResolver do

defp resolve_links_in_field(
%{"sys" => %{"id" => id, "linkType" => link_type, "type" => "Link"}} = field_value,
%{} = includes
%{} = includes,
parent_entry_id,
_visited
)
when map_size(includes) > 0 and not is_nil(id) do
Map.get(includes, link_type, [])
Expand All @@ -57,32 +96,42 @@ defmodule Contentful.Entry.LinkResolver do

# matches structs like %Asset{}, which can't be iterated through using the Enum module
# and mean links in this field have already been fully resolved anyway
defp resolve_links_in_field(%_{} = field_value, _includes), do: field_value
defp resolve_links_in_field(%_{} = field_value, _includes, _parent_entry_id, _visited),
do: field_value

# matches any other map that isn't a struct, maps can be iterated through using Enum
# map fields may still have nested links
defp resolve_links_in_field(
%{} = field_value,
%{} = includes
%{} = includes,
parent_entry_id,
visited
)
when map_size(field_value) > 0 and map_size(includes) > 0 do
field_value
|> Enum.reduce(%{}, fn {nested_field_name, nested_field_value},
field_with_nested_links_resolved ->
updated_nested_field_value = resolve_links_in_field_with_nesting(nested_field_value, includes)
updated_nested_field_value =
resolve_links_in_field_with_nesting(
nested_field_value,
includes,
parent_entry_id,
visited
)

Map.put(field_with_nested_links_resolved, nested_field_name, updated_nested_field_value)
end)
end

defp resolve_links_in_field(field_value, %{} = includes)
defp resolve_links_in_field(field_value, %{} = includes, parent_entry_id, visited)
when is_list(field_value) and length(field_value) > 0 do
field_value
|> Enum.map(fn field ->
resolve_links_in_field_with_nesting(field, includes)
resolve_links_in_field_with_nesting(field, includes, parent_entry_id, visited)
end)
end

defp resolve_links_in_field(field_value, _includes), do: field_value
defp resolve_links_in_field(field_value, _includes, _parent_entry_id, _visited), do: field_value

defp resolve_entity(nil, _link_type, fallback), do: fallback

Expand Down
11 changes: 11 additions & 0 deletions lib/contentful/metadata.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Contentful.Metadata do
defstruct [
:concepts,
:tags
]

@type t :: %Contentful.Metadata{
concepts: list(%Contentful.TaxonomyConcept{}),
tags: list(%Contentful.Tag{})
}
end
42 changes: 33 additions & 9 deletions lib/contentful/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,26 @@ defmodule Contentful.Query do
queryable
end

@doc """
adds the `links_to_entry` parameter to a query, allowing to filter entries by the entries they link to.

## Example
alias Contentful.Delivery.Entries
Entries |> links_to_entry("foobar") |> fetch_all
"""
@spec links_to_entry({Entries, list()}, String.t()) :: {Entries, list()}
def links_to_entry({Entries, parameters}, entry_id) do
{Entries, parameters |> Keyword.put(:links_to_entry, entry_id)}
end

def links_to_entry(Entries, entry_id) do
links_to_entry({Entries, []}, entry_id)
end

def links_to_entry(queryable, _entry_id) do
queryable
end

@doc """
will __resolve__ a query chain by eagerly calling the API and resolving the response immediately

Expand Down Expand Up @@ -194,7 +214,7 @@ defmodule Contentful.Query do

url =
[
space |> Delivery.url(env),
space |> Delivery.url(env, []),
queryable.endpoint()
]
|> Enum.join()
Expand Down Expand Up @@ -236,37 +256,41 @@ defmodule Contentful.Query do
id \\ nil,
space \\ Configuration.get(:space_id),
env \\ Configuration.get(:environment),
api_key \\ Configuration.get(:access_token)
api_key \\ Configuration.get(:access_token),
opts \\ []
)

def fetch_one(queryable, id, %Space{sys: %SysData{id: space_id}}, env, api_key) do
fetch_one(queryable, id, space_id, env, api_key)
def fetch_one(queryable, id, %Space{sys: %SysData{id: space_id}}, env, api_key, opts) do
fetch_one(queryable, id, space_id, env, api_key, opts)
end

def fetch_one(
queryable,
id,
space,
env,
api_key
api_key,
opts
) do
endpoint = Keyword.get(opts, :endpoint, Configuration.get(:endpoint))

url =
case {queryable, id} do
{Spaces, nil} ->
[space |> Delivery.url()]
[space |> Delivery.url(endpoint: endpoint)]

{Spaces, id} ->
[id |> Delivery.url()]
[id |> Delivery.url(endpoint: endpoint)]

{_queryable, nil} ->
raise ArgumentError, "id is missing!"

{{module, _parameters}, id} ->
# drops the parameters, as single query responses don't allow parameters
[space |> Delivery.url(env), module.endpoint(), "/#{id}"]
[space |> Delivery.url(env, endpoint: endpoint), module.endpoint(), "/#{id}"]

_ ->
[space |> Delivery.url(env), queryable.endpoint(), "/#{id}"]
[space |> Delivery.url(env, endpoint: endpoint), queryable.endpoint(), "/#{id}"]
end

# since you can pass compose into fetch one, we strip extra params here
Expand Down
2 changes: 1 addition & 1 deletion lib/contentful/request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ defmodule Contentful.Request do
|> deconstruct_filters()

options
|> Keyword.take([:limit, :skip, :include, :content_type, :query])
|> Keyword.take([:limit, :skip, :include, :content_type, :query, :links_to_entry])
|> Keyword.merge(filters)
end

Expand Down
7 changes: 7 additions & 0 deletions lib/contentful/tag.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Contentful.Tag do
defstruct [:sys]

@type t :: %Contentful.Tag{
sys: %Contentful.SysData{}
}
end
41 changes: 41 additions & 0 deletions lib/contentful/taxonomy_concept.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule Contentful.TaxonomyConcept do
alias Contentful.TaxonomyConceptScheme

defstruct [
:sys,
:uri,
:pref_label,
:alt_labels,
:hidden_labels,
:notations,
:note,
:change_note,
:definition,
:editorial_note,
:history_note,
:scope_note,
:example,
:broader,
:related,
:concept_schemes
]

@type t :: %Contentful.TaxonomyConcept{
sys: %Contentful.SysData{},
uri: String.t(),
pref_label: map(),
alt_labels: [map()],
hidden_labels: [map()],
notations: list(),
note: map(),
change_note: map(),
definition: map(),
editorial_note: map(),
history_note: map(),
scope_note: map(),
example: map(),
broader: list(__MODULE__.t()),
related: list(__MODULE__.t()),
concept_schemes: list(TaxonomyConceptScheme.t())
}
end
23 changes: 23 additions & 0 deletions lib/contentful/taxonomy_concept_scheme.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Contentful.TaxonomyConceptScheme do
alias Contentful.TaxonomyConcept

defstruct [
:sys,
:uri,
:pref_label,
:definition,
:concepts,
:top_concepts,
:total_concepts
]

@type t :: %Contentful.TaxonomyConceptScheme{
sys: %Contentful.SysData{},
uri: String.t(),
pref_label: map(),
definition: map(),
concepts: list(TaxonomyConcept.t()),
top_concepts: list(TaxonomyConcept.t()),
total_concepts: integer()
}
end
Loading