Skip to content

Commit

Permalink
Merge pull request #3 from Frameio/bryanj/GRAPH-815/update_absinthe
Browse files Browse the repository at this point in the history
[GRAPH-815] Update Absinthe Fork to 1.7.6
  • Loading branch information
bryanjos authored Jan 9, 2024
2 parents 266132b + 94bba0c commit 25c881b
Show file tree
Hide file tree
Showing 22 changed files with 387 additions and 143 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

- Bugfix: [Handle non_null(list_of(:thing)) with null list elements properly](https://github.com/absinthe-graphql/absinthe/pull/1259)
- Bugfix: [More non null result handling improvements](https://github.com/absinthe-graphql/absinthe/pull/1275)

## 1.7.4

- Feature: Support Dataloader 2.0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Last Updated](https://img.shields.io/github/last-commit/absinthe-graphql/absinthe.svg)](https://github.com/absinthe-graphql/absinthe/commits/master)

[GraphQL](https://github.com/graphql-elixir/graphql) implementation for Elixir.
[GraphQL](https://graphql.org) implementation for Elixir.

Goals:

Expand Down
41 changes: 22 additions & 19 deletions guides/client/apollo.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ An Apollo client manages its connection to the GraphQL server using [links](http
Using Apollo with an HTTP link does not require any Absinthe-specific configuration. You can create an HTTP link pointed at your Absinthe server as follows:

```javascript
import ApolloClient from "apollo-client";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";

// Create an HTTP link to the Absinthe server.
const link = createHttpLink({
Expand All @@ -31,10 +29,8 @@ const client = new ApolloClient({
You may find that you need to modify the HTTP request that Apollo makes -- for example, if you wish to send the value of a particular cookie in the `Authorization` header. The `setContext` helper allows you to do this, and also demonstrates how links in Apollo can be chained.

```javascript
import ApolloClient from "apollo-client";
import { createHttpLink } from "apollo-link-http";
import { setContext } from "apollo-link-context";
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import Cookies from "js-cookie";

// Create an HTTP link to the Absinthe server.
Expand Down Expand Up @@ -77,11 +73,10 @@ const client = new ApolloClient({
An HTTP link is suitable for many basic use cases, but if you require two-way communication between the server and the client, you will need to use a websocket link. The most common use case is a client that needs to use GraphQL subscriptions to receive updates from the server when particular events occur. To implement a websocket link, we will need to use the [`@absinthe/socket`](https://www.npmjs.com/package/@absinthe/socket) and [`@absinthe/socket-apollo-link`](https://www.npmjs.com/package/@absinthe/socket-apollo-link) packages.

```javascript
import ApolloClient from "apollo-client";
import { ApolloClient, InMemoryCache } from "@apollo/client";
import * as AbsintheSocket from "@absinthe/socket";
import { createAbsintheSocketLink } from "@absinthe/socket-apollo-link";
import { Socket as PhoenixSocket } from "phoenix";
import { InMemoryCache } from "apollo-cache-inmemory";
import Cookies from "js-cookie";

// Create a standard Phoenix websocket connection. If you need
Expand Down Expand Up @@ -122,11 +117,10 @@ You may find that you periodically need to reconnect the websocket with differen
Note that this solution (reconnecting with `phoenixSocket.conn.close();`) is somewhat unstable because it relies upon an implementation detail of the Phoenix socket. Ideally, a future version of the Phoenix package might add a public API method to reconnect the websocket with new parameters.

```javascript
import ApolloClient from "apollo-client";
import { ApolloClient, InMemoryCache } from "@apollo/client";
import * as AbsintheSocket from "@absinthe/socket";
import { createAbsintheSocketLink } from "@absinthe/socket-apollo-link";
import { Socket as PhoenixSocket } from "phoenix";
import { InMemoryCache } from "apollo-cache-inmemory";
import Cookies from "js-cookie";

// Create a standard Phoenix websocket connection. If you need
Expand Down Expand Up @@ -171,18 +165,20 @@ phoenixSocket.conn.close();

## Using both HTTP and websocket links

A common configuration for Apollo client applications is to use both HTTP and websocket links -- HTTP for queries and mutations, and a websocket for subscriptions. We can implement this in our client using [directional composition with Apollo's `split` helper](https://www.apollographql.com/docs/link/composition#directional).
A common configuration for Apollo client applications is to use both HTTP and websocket links -- HTTP for queries and mutations, and a websocket for subscriptions. We can implement this in our client using [directional composition with Apollo's `split` helper](https://www.apollographql.com/docs/react/api/link/introduction/#directional-composition).

```javascript
import ApolloClient from "apollo-client";
import { createHttpLink } from "apollo-link-http";
import { setContext } from "apollo-link-context";
import {
ApolloClient,
InMemoryCache,
createHttpLink,
split
} from "@apollo/client";
import { getMainDefinition } from "@apollo/client/utilities";
import { setContext } from "@apollo/client/link/context";
import * as AbsintheSocket from "@absinthe/socket";
import { createAbsintheSocketLink } from "@absinthe/socket-apollo-link";
import { Socket as PhoenixSocket } from "phoenix";
import { hasSubscription } from "@jumpn/utils-graphql";
import { split } from "apollo-link";
import { InMemoryCache } from "apollo-cache-inmemory";
import Cookies from "js-cookie";

// Create an HTTP link to the Absinthe server.
Expand Down Expand Up @@ -230,7 +226,14 @@ const websocketLink = createAbsintheSocketLink(absintheSocket);
// If the query contains a subscription, send it through the
// websocket link. Otherwise, send it through the HTTP link.
const link = split(
operation => hasSubscription(operation.query),
(operation) => {
const definition = getMainDefinition(query);

return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
websocketLink,
authedHttpLink
);
Expand Down
2 changes: 1 addition & 1 deletion guides/introduction/overview.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Overview

Absinthe is the GraphQL toolkit for Elixir, an implementation of the [GraphQL specification](https://github.com/graphql-elixir/graphql) built to suit the language's capabilities and idiomatic style.
Absinthe is the GraphQL toolkit for Elixir, an implementation of the [GraphQL specification](https://spec.graphql.org) built to suit the language's capabilities and idiomatic style.

The Absinthe project consists of several complementary packages. You can find the full listing on the [absinthe-graphql](https://github.com/absinthe-graphql) GitHub organization page.

Expand Down
2 changes: 1 addition & 1 deletion lib/absinthe/blueprint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ defmodule Absinthe.Blueprint do
source: nil | String.t() | Absinthe.Language.Source.t(),
execution: Blueprint.Execution.t(),
result: result_t,
initial_phases: [Absinthe.Phase.t()]
initial_phases: [Absinthe.Phase.t()] | {module(), atom, list()}
}

@type result_t :: %{
Expand Down
1 change: 1 addition & 0 deletions lib/absinthe/blueprint/schema/enum_type_definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ defmodule Absinthe.Blueprint.Schema.EnumTypeDefinition do
value: value_def.value,
enum_identifier: type_def.identifier,
__reference__: value_def.__reference__,
__private__: value_def.__private__,
description: value_def.description,
deprecation: value_def.deprecation
}
Expand Down
4 changes: 3 additions & 1 deletion lib/absinthe/blueprint/schema/object_type_definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ defmodule Absinthe.Blueprint.Schema.ObjectTypeDefinition do
description: arg_def.description,
type: Blueprint.TypeReference.to_type(arg_def.type, schema),
default_value: arg_def.default_value,
deprecation: arg_def.deprecation
deprecation: arg_def.deprecation,
__reference__: arg_def.__reference__,
__private__: arg_def.__private__
}

{arg_def.identifier, arg}
Expand Down
4 changes: 3 additions & 1 deletion lib/absinthe/blueprint/schema/union_type_definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ defmodule Absinthe.Blueprint.Schema.UnionTypeDefinition do
description: arg_def.description,
type: Blueprint.TypeReference.to_type(arg_def.type, schema),
default_value: arg_def.default_value,
deprecation: arg_def.deprecation
deprecation: arg_def.deprecation,
__reference__: arg_def.__reference__,
__private__: arg_def.__private__
}

{arg_def.identifier, arg}
Expand Down
61 changes: 39 additions & 22 deletions lib/absinthe/phase/document/execution/resolution.ex
Original file line number Diff line number Diff line change
Expand Up @@ -285,20 +285,29 @@ defmodule Absinthe.Phase.Document.Execution.Resolution do
|> propagate_null_trimming
end

defp maybe_add_non_null_error([], values, %Type.NonNull{of_type: %Type.List{}}) do
defp maybe_add_non_null_error(errors, value, type, path \\ [])

defp maybe_add_non_null_error([], nil, %Type.NonNull{}, []) do
["Cannot return null for non-nullable field"]
end

defp maybe_add_non_null_error([], nil, %Type.NonNull{}, path) do
[%{message: "Cannot return null for non-nullable field", path: Enum.reverse(path)}]
end

defp maybe_add_non_null_error([], value, %Type.NonNull{of_type: %Type.List{} = type}, path) do
maybe_add_non_null_error([], value, type, path)
end

defp maybe_add_non_null_error([], [_ | _] = values, %Type.List{of_type: type}, path) do
values
|> Enum.with_index()
|> Enum.filter(&is_nil(elem(&1, 0)))
|> Enum.map(fn {_value, index} ->
%{message: "Cannot return null for non-nullable field", path: [index]}
|> Enum.flat_map(fn {value, index} ->
maybe_add_non_null_error([], value, type, [index | path])
end)
end

defp maybe_add_non_null_error([], nil, %Type.NonNull{}) do
["Cannot return null for non-nullable field"]
end

defp maybe_add_non_null_error(errors, _, _) do
defp maybe_add_non_null_error(errors, _, _, _path) do
errors
end

Expand Down Expand Up @@ -350,28 +359,36 @@ defmodule Absinthe.Phase.Document.Execution.Resolution do
false
end

defp non_null_list_violation?(%{values: values}) do
Enum.find(values, &non_null_list_violation?/1)
end

# FIXME: Not super happy with this lookup process.
# Also it would be nice if we could use the same function as above.
defp non_null_list_violation?(%{
value: nil,
emitter: %{schema_node: %{type: %Type.List{of_type: %Type.NonNull{}}}}
}) do
true
defp non_null_list_violation?(%{value: nil, emitter: %{schema_node: %{type: type}}}) do
!null_allowed_in_list?(type)
end

defp non_null_list_violation?(%{
value: nil,
emitter: %{
schema_node: %{type: %Type.NonNull{of_type: %Type.List{of_type: %Type.NonNull{}}}}
}
}) do
true
defp non_null_list_violation?(_) do
false
end

defp non_null_list_violation?(_) do
defp null_allowed_in_list?(%Type.List{of_type: wrapped_type}) do
null_allowed_in_list?(wrapped_type)
end

defp null_allowed_in_list?(%Type.NonNull{of_type: %Type.List{of_type: wrapped_type}}) do
null_allowed_in_list?(wrapped_type)
end

defp null_allowed_in_list?(%Type.NonNull{of_type: _wrapped_type}) do
false
end

defp null_allowed_in_list?(_type) do
true
end

defp add_errors(result, errors, fun) do
Enum.reduce(errors, result, fun)
end
Expand Down
3 changes: 1 addition & 2 deletions lib/absinthe/phase/subscription/subscribe_self.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ defmodule Absinthe.Phase.Subscription.SubscribeSelf do
field_keys = get_field_keys(field, config)
subscription_id = get_subscription_id(config, blueprint, options)

for field_key <- field_keys,
do: Absinthe.Subscription.subscribe(pubsub, field_key, subscription_id, blueprint)
Absinthe.Subscription.subscribe(pubsub, field_keys, subscription_id, blueprint)

{:replace, blueprint,
[
Expand Down
10 changes: 5 additions & 5 deletions lib/absinthe/schema/notation/sdl_render.ex
Original file line number Diff line number Diff line change
Expand Up @@ -351,30 +351,30 @@ defmodule Absinthe.Schema.Notation.SDL.Render do

# Render Helpers

defp render_list(items, type_definitions, seperator \\ line())
defp render_list(items, type_definitions, separator \\ line())

# Workaround for `values` macro which temporarily defines
# values as raw atoms to support dynamic schemas
defp render_list([first | _] = items, type_definitions, seperator) when is_atom(first) do
defp render_list([first | _] = items, type_definitions, separator) when is_atom(first) do
items
|> Enum.map(
&%Blueprint.Schema.EnumValueDefinition{
value: &1,
name: String.upcase(to_string(&1))
}
)
|> render_list(type_definitions, seperator)
|> render_list(type_definitions, separator)
end

defp render_list(items, type_definitions, seperator) do
defp render_list(items, type_definitions, separator) do
items = Enum.reject(items, &(&1.module in @skip_modules))

splitter =
items
|> Enum.any?(&(&1.description not in ["", nil]))
|> case do
true -> [nest(line(), :reset), line()]
false -> [seperator]
false -> [separator]
end

items
Expand Down
57 changes: 42 additions & 15 deletions lib/absinthe/subscription.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ defmodule Absinthe.Subscription do
@doc """
Build a child specification for subscriptions.
In order to use supscriptions in your application, you must add
In order to use subscriptions in your application, you must add
`Absinthe.Subscription` to your supervision tree after your endpoint.
See `guides/subscriptions.md` for more information on how to get up and
Expand Down Expand Up @@ -140,31 +140,48 @@ defmodule Absinthe.Subscription do
defp fetch_fields(_, _), do: []

@doc false
def subscribe(pubsub, field_key, doc_id, doc) do
def subscribe(pubsub, field_keys, doc_id, doc) do
field_keys = List.wrap(field_keys)

registry = pubsub |> registry_name

doc_value = {
doc_id,
%{
initial_phases: PipelineSerializer.pack(doc.initial_phases),
source: doc.source
}
doc_value = %{
initial_phases: PipelineSerializer.pack(doc.initial_phases),
source: doc.source
}

{:ok, _} = Registry.register(registry, field_key, doc_value)
{:ok, _} = Registry.register(registry, {self(), doc_id}, field_key)
pdict_add_fields(doc_id, field_keys)

for field_key <- field_keys do
{:ok, _} = Registry.register(registry, field_key, doc_id)
end

{:ok, _} = Registry.register(registry, doc_id, doc_value)
end

defp pdict_fields(doc_id) do
Process.get({__MODULE__, doc_id}, [])
end

defp pdict_add_fields(doc_id, field_keys) do
Process.put({__MODULE__, doc_id}, field_keys ++ pdict_fields(doc_id))
end

defp pdict_delete_fields(doc_id) do
Process.delete({__MODULE__, doc_id})
end

@doc false
def unsubscribe(pubsub, doc_id) do
registry = pubsub |> registry_name
self = self()

for {^self, field_key} <- Registry.lookup(registry, {self, doc_id}) do
Registry.unregister_match(registry, field_key, {doc_id, :_})
for field_key <- pdict_fields(doc_id) do
Registry.unregister(registry, field_key)
end

Registry.unregister(registry, {self, doc_id})
Registry.unregister(registry, doc_id)

pdict_delete_fields(doc_id)
:ok
end

Expand All @@ -173,7 +190,17 @@ defmodule Absinthe.Subscription do
pubsub
|> registry_name
|> Registry.lookup(key)
|> Map.new(fn {_, {doc_id, doc}} ->
|> then(fn doc_ids ->
pubsub
|> registry_name
|> Registry.select(
# We compose a list of match specs that basically mean "lookup all keys
# in the doc_ids list"
for {_, doc_id} <- doc_ids,
do: {{:"$1", :_, :"$2"}, [{:==, :"$1", doc_id}], [{{:"$1", :"$2"}}]}
)
end)
|> Map.new(fn {doc_id, doc} ->
doc = Map.update!(doc, :initial_phases, &PipelineSerializer.unpack/1)

{doc_id, doc}
Expand Down
Loading

0 comments on commit 25c881b

Please sign in to comment.