-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
OpenAPI: first pass on Plugins API - Shared Links (#3378)
* Update depenedencies: OpenAPISpex + cursor based pagination * Update formatter config * Add internal server error implementation * Test errors * Implement pagination interface * Implement Plugins API module macros * Implement Public API base URI (to be used with path helpers once called from within forwarded router's scope) * Implement OpenAPI specs + schemas * Implement Shared Links context module * Add pagination and error views * Add Shared Link view * Implement Shared Link controller * Expose SharedLink.t() spec * Implement separate router for the Plugins API * Update moduledocs * Always wrap resource objects with `data` * Update moduledoc * Use open-api-spex/open_api_spex#425 due to open-api-spex/open_api_spex#92 * Rely on BASE_URL for swagger-ui server definition * Fixup goals migration * Migrate broken goals before deleting dupes * Remove bypassing test rate limiting for which there's none anyway * Move the context module under `Plausible.` namespace * Bring back conn assignment to PluginsAPICase template * Update test/plausible_web/plugins/api/controllers/shared_links_test.exs Co-authored-by: Uku Taht <Uku.taht@gmail.com> * Update renamed aliases * Seed static token for development purposes * Delegate Plugins API 500s to a familiar shape * Simplify with statement --------- Co-authored-by: Uku Taht <Uku.taht@gmail.com>
- Loading branch information
Showing
33 changed files
with
1,072 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
defmodule Plausible.Plugins.API.Context.SharedLinks do | ||
@moduledoc """ | ||
Plugins API Context module for Shared Links. | ||
All high level Shared Links operations should be implemented here. | ||
""" | ||
import Ecto.Query | ||
import Plausible.Plugins.API.Pagination | ||
|
||
alias Plausible.Repo | ||
|
||
@spec get_shared_links(Plausible.Site.t(), map()) :: {:ok, Paginator.Page.t()} | ||
def get_shared_links(site, params) do | ||
query = | ||
from l in Plausible.Site.SharedLink, | ||
where: l.site_id == ^site.id, | ||
order_by: [desc: l.id] | ||
|
||
{:ok, paginate(query, params, cursor_fields: [{:id, :desc}])} | ||
end | ||
|
||
@spec get(Plausible.Site.t(), pos_integer() | String.t()) :: nil | Plausible.Site.SharedLink.t() | ||
def get(site, id) when is_integer(id) do | ||
get_by_id(site, id) | ||
end | ||
|
||
def get(site, name) when is_binary(name) do | ||
get_by_name(site, name) | ||
end | ||
|
||
@spec get_or_create(Plausible.Site.t(), String.t(), String.t() | nil) :: | ||
{:ok, Plausible.Site.SharedLink.t()} | ||
def get_or_create(site, name, password \\ nil) do | ||
case get_by_name(site, name) do | ||
nil -> Plausible.Sites.create_shared_link(site, name, password) | ||
shared_link -> {:ok, shared_link} | ||
end | ||
end | ||
|
||
defp get_by_id(site, id) do | ||
Repo.one( | ||
from l in Plausible.Site.SharedLink, | ||
where: l.site_id == ^site.id, | ||
where: l.id == ^id | ||
) | ||
end | ||
|
||
defp get_by_name(site, name) do | ||
Repo.one( | ||
from l in Plausible.Site.SharedLink, | ||
where: l.site_id == ^site.id, | ||
where: l.name == ^name | ||
) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
defmodule Plausible.Plugins.API.Pagination do | ||
@moduledoc """ | ||
Cursor-based pagination for the Plugins API. | ||
Can be moved to another namespace in case used elsewhere. | ||
""" | ||
|
||
@limit 10 | ||
@maximum_limit 100 | ||
|
||
@spec paginate(Ecto.Queryable.t(), map(), Keyword.t(), Keyword.t()) :: Paginator.Page.t() | ||
def paginate(queryable, params, opts, repo_opts \\ []) do | ||
opts = Keyword.merge([limit: @limit, maximum_limit: @maximum_limit], opts) | ||
|
||
Paginator.paginate( | ||
queryable, | ||
Keyword.merge(opts, to_pagination_opts(params)), | ||
Plausible.Repo, | ||
repo_opts | ||
) | ||
end | ||
|
||
defp to_pagination_opts(params) do | ||
Enum.reduce(params, Keyword.new(), fn | ||
{"after", cursor}, acc -> | ||
Keyword.put(acc, :after, cursor) | ||
|
||
{"before", cursor}, acc -> | ||
Keyword.put(acc, :before, cursor) | ||
|
||
{"limit", limit}, acc -> | ||
limit = to_int(limit) | ||
|
||
if limit > 0 and limit <= @maximum_limit do | ||
Keyword.put(acc, :limit, limit) | ||
else | ||
acc | ||
end | ||
end) | ||
end | ||
|
||
defp to_int(x) when is_binary(x), do: String.to_integer(x) | ||
defp to_int(x) when is_integer(x), do: x | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
defmodule PlausibleWeb.Plugins.API do | ||
@moduledoc """ | ||
Plausible Plugins API | ||
""" | ||
|
||
@doc """ | ||
Returns the API base URI, so that complete URLs can | ||
be generated from forwared Router helpers. | ||
""" | ||
@spec base_uri() :: URI.t() | ||
def base_uri() do | ||
PlausibleWeb.Endpoint.url() | ||
|> Path.join("/api/plugins") | ||
|> URI.new!() | ||
end | ||
end |
109 changes: 109 additions & 0 deletions
109
lib/plausible_web/plugins/api/controllers/shared_links.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
defmodule PlausibleWeb.Plugins.API.Controllers.SharedLinks do | ||
@moduledoc """ | ||
Controller for the Shared Link resource under Plugins API | ||
""" | ||
use PlausibleWeb, :plugins_api_controller | ||
|
||
operation(:index, | ||
summary: "Retrieve Shared Links", | ||
parameters: [ | ||
limit: [in: :query, type: :integer, description: "Maximum entries per page", example: 10], | ||
after: [ | ||
in: :query, | ||
type: :string, | ||
description: "Cursor value to seek after - generated internally" | ||
], | ||
before: [ | ||
in: :query, | ||
type: :string, | ||
description: "Cursor value to seek before - generated internally" | ||
] | ||
], | ||
responses: %{ | ||
ok: {"Shared Links response", "application/json", Schemas.SharedLink.ListResponse}, | ||
unauthorized: {"Unauthorized", "application/json", Schemas.Unauthorized} | ||
} | ||
) | ||
|
||
@spec index(Plug.Conn.t(), %{}) :: Plug.Conn.t() | ||
def index(conn, _params) do | ||
{:ok, pagination} = | ||
Context.SharedLinks.get_shared_links(conn.assigns.authorized_site, conn.query_params) | ||
|
||
conn | ||
|> put_view(Views.SharedLink) | ||
|> render("index.json", %{pagination: pagination}) | ||
end | ||
|
||
operation(:create, | ||
summary: "Create Shared Link", | ||
request_body: {"Shared Link params", "application/json", Schemas.SharedLink.CreateRequest}, | ||
responses: %{ | ||
created: {"Shared Link", "application/json", Schemas.SharedLink}, | ||
unauthorized: {"Unauthorized", "application/json", Schemas.Unauthorized}, | ||
unprocessable_entity: | ||
{"Unprocessable entity", "application/json", Schemas.UnprocessableEntity} | ||
} | ||
) | ||
|
||
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t() | ||
def create( | ||
%{ | ||
private: %{ | ||
open_api_spex: %{ | ||
body_params: %Schemas.SharedLink.CreateRequest{name: name, password: password} | ||
} | ||
} | ||
} = conn, | ||
_params | ||
) do | ||
site = conn.assigns.authorized_site | ||
|
||
{:ok, shared_link} = Context.SharedLinks.get_or_create(site, name, password) | ||
|
||
conn | ||
|> put_view(Views.SharedLink) | ||
|> put_status(:created) | ||
|> put_resp_header("location", shared_links_url(base_uri(), :get, shared_link.id)) | ||
|> render("shared_link.json", shared_link: shared_link, authorized_site: site) | ||
end | ||
|
||
operation(:get, | ||
summary: "Retrieve Shared Link by ID", | ||
parameters: [ | ||
id: [ | ||
in: :path, | ||
type: :integer, | ||
description: "Shared Link ID", | ||
example: 123, | ||
required: true | ||
] | ||
], | ||
responses: %{ | ||
created: {"Shared Link", "application/json", Schemas.SharedLink}, | ||
not_found: {"NotFound", "application/json", Schemas.NotFound}, | ||
unauthorized: {"Unauthorized", "application/json", Schemas.Unauthorized}, | ||
unprocessable_entity: | ||
{"Unprocessable entity", "application/json", Schemas.UnprocessableEntity} | ||
} | ||
) | ||
|
||
@spec get(Plug.Conn.t(), map()) :: Plug.Conn.t() | ||
def get(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _params) do | ||
site = conn.assigns.authorized_site | ||
|
||
case Context.SharedLinks.get(site, id) do | ||
nil -> | ||
conn | ||
|> put_view(Views.Error) | ||
|> put_status(:not_found) | ||
|> render("404.json") | ||
|
||
shared_link -> | ||
conn | ||
|> put_view(Views.SharedLink) | ||
|> put_status(:ok) | ||
|> render("shared_link.json", shared_link: shared_link, authorized_site: site) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
defmodule PlausibleWeb.Plugins.API.Router do | ||
use PlausibleWeb, :router | ||
|
||
pipeline :auth do | ||
plug(PlausibleWeb.Plugs.AuthorizePluginsAPI) | ||
end | ||
|
||
pipeline :api do | ||
plug(:accepts, ["json"]) | ||
plug(OpenApiSpex.Plug.PutApiSpec, module: PlausibleWeb.Plugins.API.Spec) | ||
end | ||
|
||
scope "/spec" do | ||
pipe_through(:api) | ||
get("/openapi", OpenApiSpex.Plug.RenderSpec, []) | ||
get("/swagger-ui", OpenApiSpex.Plug.SwaggerUI, path: "/api/plugins/spec/openapi") | ||
end | ||
|
||
scope "/v1", PlausibleWeb.Plugins.API.Controllers do | ||
pipe_through([:api, :auth]) | ||
|
||
get("/shared_links", SharedLinks, :index) | ||
get("/shared_links/:id", SharedLinks, :get) | ||
post("/shared_links", SharedLinks, :create) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
defmodule PlausibleWeb.Plugins.API.Schemas.Error do | ||
@moduledoc """ | ||
OpenAPI schema for an error included in a response | ||
""" | ||
|
||
use PlausibleWeb, :open_api_schema | ||
|
||
OpenApiSpex.schema(%{ | ||
description: """ | ||
An explanation of an error that occurred within the Plugins API | ||
""", | ||
type: :object, | ||
required: [:detail], | ||
properties: %{detail: %OpenApiSpex.Schema{type: :string}} | ||
}) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
defmodule PlausibleWeb.Plugins.API.Schemas.Link do | ||
@moduledoc """ | ||
OpenAPI Link schema | ||
""" | ||
use PlausibleWeb, :open_api_schema | ||
|
||
OpenApiSpex.schema(%{ | ||
title: "Link", | ||
type: :object, | ||
required: [:url], | ||
properties: %{url: %OpenApiSpex.Schema{type: :string}} | ||
}) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
defmodule PlausibleWeb.Plugins.API.Schemas.NotFound do | ||
@moduledoc """ | ||
OpenAPI schema for a generic 404 response | ||
""" | ||
|
||
use PlausibleWeb, :open_api_schema | ||
|
||
OpenApiSpex.schema(%{ | ||
description: """ | ||
The response that is returned when the user makes a request to a non-existing resource | ||
""", | ||
type: :object, | ||
title: "NotFoundError", | ||
required: [:errors], | ||
properties: %{ | ||
errors: %OpenApiSpex.Schema{ | ||
items: Schemas.Error, | ||
type: :array | ||
} | ||
} | ||
}) | ||
end |
25 changes: 25 additions & 0 deletions
25
lib/plausible_web/plugins/api/schemas/pagination_metadata.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
defmodule PlausibleWeb.Plugins.API.Schemas.PaginationMetadata do | ||
@moduledoc """ | ||
Pagination metadata OpenAPI schema | ||
""" | ||
use PlausibleWeb, :open_api_schema | ||
|
||
OpenApiSpex.schema(%{ | ||
title: "PaginationMetadata", | ||
description: "Pagination meta data", | ||
type: :object, | ||
required: [:has_next_page, :has_prev_page], | ||
properties: %{ | ||
has_next_page: %OpenApiSpex.Schema{type: :boolean}, | ||
has_prev_page: %OpenApiSpex.Schema{type: :boolean}, | ||
links: %OpenApiSpex.Schema{ | ||
items: Schemas.Link, | ||
properties: %{ | ||
next: %OpenApiSpex.Reference{"$ref": "#/components/schemas/Link"}, | ||
prev: %OpenApiSpex.Reference{"$ref": "#/components/schemas/Link"} | ||
}, | ||
type: :object | ||
} | ||
} | ||
}) | ||
end |
Oops, something went wrong.