Packages
electric
1.0.0-beta.14
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.16
1.4.16-beta-1
1.4.15
1.4.14
1.4.13
1.4.12
1.4.11
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
retired
1.1.4
retired
1.1.3
retired
1.1.2
1.1.1
1.1.0
1.0.24
1.0.23
1.0.22
1.0.21
1.0.20
1.0.19
1.0.18
1.0.17
1.0.15
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-beta.23
1.0.0-beta.22
1.0.0-beta.20
1.0.0-beta.19
1.0.0-beta.18
1.0.0-beta.17
1.0.0-beta.16
1.0.0-beta.15
1.0.0-beta.14
1.0.0-beta.13
1.0.0-beta.12
1.0.0-beta.11
1.0.0-beta.10
1.0.0-beta.9
1.0.0-beta.8
1.0.0-beta.7
1.0.0-beta.6
1.0.0-beta.5
1.0.0-beta.4
1.0.0-beta.3
1.0.0-beta.2
1.0.0-beta.1
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.5.2
0.4.4
Postgres sync engine. Sync little subsets of your Postgres data into local apps and services.
Current section
Files
Jump to
Current section
Files
lib/electric/shapes/api/response.ex
defmodule Electric.Shapes.Api.Response do
alias Electric.Shapes.Api
alias Electric.Shapes.Shape
alias Electric.Telemetry.OpenTelemetry
require Logger
defstruct [
:handle,
:offset,
:shape_definition,
chunked: false,
up_to_date: false,
status: 200,
trace_attrs: %{},
body: []
]
@type shape_handle :: Electric.ShapeCacheBehaviour.shape_handle()
@type t() :: %__MODULE__{
handle: nil | shape_handle(),
offset: nil | Electric.Replication.LogOffset.t(),
shape_definition: nil | Shape.t(),
chunked: boolean(),
up_to_date: boolean(),
status: pos_integer(),
trace_attrs: %{optional(atom()) => term()},
body: Enum.t()
}
@shape_definition_mismatch %{
message:
"The specified shape definition and handle do not match. " <>
"Please ensure the shape definition is correct or omit " <>
"the shape handle from the request to obtain a new one."
}
def shape_definition_mismatch(request) do
error(request, @shape_definition_mismatch)
end
def error(api_or_request, message, args \\ [])
@spec error(Api.t() | Api.Request.t(), term(), keyword()) :: t()
def error(%Api{} = api, message, args) do
opts =
args
|> Keyword.put_new(:status, 400)
|> Keyword.put(:body, error_body(api, message, args))
struct(__MODULE__, opts)
end
def error(%Api.Request{} = request, message, args) do
opts =
args
|> Keyword.put_new(:status, 400)
|> Keyword.put(:body, error_body(request, message, args))
|> Keyword.put(:shape_definition, request.params.shape_definition)
struct(__MODULE__, opts)
end
def invalid_request(api_or_request, args) do
error(api_or_request, "Invalid request", args)
end
defp error_body(api_or_request, message, args) when is_binary(message) do
error_body(api_or_request, %{message: message}, args)
end
defp error_body(api_or_request, message, args) do
body =
if errors = Keyword.get(args, :errors) do
Map.put(message, :errors, errors)
else
message
end
Api.encode_message(api_or_request, body)
end
@spec send(Plug.Conn.t(), t()) :: Plug.Conn.t()
def send(%Plug.Conn{} = conn, %__MODULE__{chunked: false} = response) do
conn
|> put_resp_headers(response)
|> Plug.Conn.send_resp(response.status, Enum.into(response.body, []))
end
def send(%Plug.Conn{} = conn, %__MODULE__{} = response) do
conn
|> put_resp_headers(response)
|> send_stream(response)
end
defp put_resp_headers(conn, response) do
conn
|> put_location_header(response)
|> put_shape_handle_header(response)
|> put_up_to_date_header(response)
|> put_offset_header(response)
end
defp put_location_header(conn, %__MODULE__{status: 409} = response) do
params = [
table: Electric.Utils.relation_to_sql(response.shape_definition.root_table),
handle: response.handle,
offset: "-1"
]
query = URI.encode_query(params)
Plug.Conn.put_resp_header(
conn,
"location",
"#{conn.request_path}?#{query}"
)
end
defp put_location_header(conn, %__MODULE__{} = _response) do
conn
end
defp put_shape_handle_header(conn, %__MODULE__{handle: nil}) do
conn
end
defp put_shape_handle_header(conn, response) do
Plug.Conn.put_resp_header(conn, "electric-handle", response.handle)
end
defp put_up_to_date_header(conn, %__MODULE__{up_to_date: true}) do
Plug.Conn.put_resp_header(conn, "electric-up-to-date", "")
end
defp put_up_to_date_header(conn, %__MODULE__{up_to_date: false}) do
Plug.Conn.delete_resp_header(conn, "electric-up-to-date")
end
defp put_offset_header(conn, %__MODULE__{offset: nil}) do
conn
end
defp put_offset_header(conn, %__MODULE__{offset: offset}) do
Plug.Conn.put_resp_header(conn, "electric-offset", "#{offset}")
end
defp send_stream(%Plug.Conn{} = conn, %__MODULE__{body: stream, status: status}) do
stack_id = Api.stack_id(conn.assigns.request)
conn = Plug.Conn.send_chunked(conn, status)
{conn, bytes_sent} =
Enum.reduce_while(stream, {conn, 0}, fn chunk, {conn, bytes_sent} ->
chunk_size = IO.iodata_length(chunk)
OpenTelemetry.with_span(
"shape_get.plug.stream_chunk",
[chunk_size: chunk_size],
stack_id,
fn ->
case Plug.Conn.chunk(conn, chunk) do
{:ok, conn} ->
{:cont, {conn, bytes_sent + chunk_size}}
{:error, "closed"} ->
error_str = "Connection closed unexpectedly while streaming response"
conn = Plug.Conn.assign(conn, :error_str, error_str)
{:halt, {conn, bytes_sent}}
{:error, reason} ->
error_str = "Error while streaming response: #{inspect(reason)}"
Logger.error(error_str)
conn = Plug.Conn.assign(conn, :error_str, error_str)
{:halt, {conn, bytes_sent}}
end
end
)
end)
Plug.Conn.assign(conn, :streaming_bytes_sent, bytes_sent)
end
end