ash-project / ash_json_api

The JSON:API extension for the Ash Framework
https://hexdocs.pm/ash_json_api
MIT License
63 stars 43 forks source link

Mismatch in filter conditions generates a resolver error in Swagger UI #190

Closed sevenseacat closed 5 months ago

sevenseacat commented 5 months ago

Describe the bug

When upgrading to the latest version of ash_json_api to test out the new filteing functionality, I see the following error generated in Swagger UI:

Resolver error at paths./api/artists.get.parameters.0.schema.properties.albums.$ref
Could not resolve reference: Could not resolve pointer: /components/schemas/album-filter does not exist in document

This pointer appears exactly once in my OpenAPI-generated spec:

image

To Reproduce

My domain json_api config:

json_api do
    prefix "/api"

    routes do
      base_route "/artists", Tunez.Music.Artist do
        get :read
        index :search
        post :create
        patch :update
        delete :destroy
        related :albums, :read, primary?: true
      end

      base_route "/albums", Tunez.Music.Album do
        post :create
        patch :update
        delete :destroy
        related :tracks, :read, primary?: true
      end
    end
  end

Tunez.Music.Artist:

  json_api do
    type "artist"
    includes [:albums]
  end

  relationships do
    has_many :albums, Tunez.Music.Album do
      public? true
    end

Tunez.Music.Album:

  json_api do
    type "album"
  end

Expected behavior

No error in Swagger UI!

** Runtime

Additional context

As per your notes in Discord:

It's probably a mismatch in the condition for generating the filter schema and the condition for rendering it.

    defp define_filter?(domains, resource) do
      if AshJsonApi.Resource.Info.derive_filter?(resource) do
        resource
        |> AshJsonApi.Resource.Info.routes(domains)
        |> Enum.any?(fn route ->
          route.type == :index && read_action?(resource, route) && route.derive_filter?
        end)
      else
        false
      end
    end

That is insufficient, because anything that relates to it can be filtered via the relationship

    defp define_filter?(domains, resource) do
      if AshJsonApi.Resource.Info.derive_filter?(resource) do
        something_relates_to(domains, resource) || has_index_route?(domains, resource)
      else
        false
      end
    end

    defp something_relates_to(domains, resource) do
      domains
      |> Stream.flat_map(&Ash.Domain.Info.resources/1)
      |> Stream.flat_map(&Ash.Resource.Info.relationships/1)
      |> Stream.filter(& &1.public?)
      |> Enum.any?(&(&1.destination == resource))
    end

    defp has_index_route?(domains, resource) do
      resource
      |> AshJsonApi.Resource.Info.routes(domains)
      |> Enum.any?(fn route ->
        route.type == :index && read_action?(resource, route) && route.derive_filter?
      end)
    end

Something like this is required