Spaces:
Sleeping
Sleeping
hiett commited on
Commit ·
a8d7371
1
Parent(s): 3771745
Response encoding for single items and lists
Browse files- example/src/index.ts +19 -4
- lib/srh/http/base_router.ex +31 -9
- lib/srh/http/request_validator.ex +15 -0
- lib/srh/http/result_encoder.ex +66 -0
example/src/index.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
| 1 |
import {Redis} from "@upstash/redis";
|
| 2 |
|
| 3 |
const redis = new Redis({
|
| 4 |
-
url:
|
| 5 |
token: "example_token",
|
| 6 |
-
responseEncoding:
|
| 7 |
});
|
| 8 |
|
| 9 |
(async () => {
|
| 10 |
-
await redis.set("key", "value");
|
| 11 |
-
const value = await redis.get("
|
| 12 |
console.log(value); // value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
})();
|
|
|
|
| 1 |
import {Redis} from "@upstash/redis";
|
| 2 |
|
| 3 |
const redis = new Redis({
|
| 4 |
+
url: "http://127.0.0.1:8080",
|
| 5 |
token: "example_token",
|
| 6 |
+
// responseEncoding: true,
|
| 7 |
});
|
| 8 |
|
| 9 |
(async () => {
|
| 10 |
+
// await redis.set("key", "value");
|
| 11 |
+
const value = await redis.get("foo");
|
| 12 |
console.log(value); // value
|
| 13 |
+
|
| 14 |
+
// Run a pipeline operation
|
| 15 |
+
const pipelineResponse = await redis.pipeline()
|
| 16 |
+
.set("amazing-key", "bar")
|
| 17 |
+
.get("amazing-key")
|
| 18 |
+
.del("amazing-other-key")
|
| 19 |
+
.del("random-key-that-doesnt-exist")
|
| 20 |
+
.srandmember("random-key-that-doesnt-exist")
|
| 21 |
+
.sadd("amazing-set", "item1", "item2", "item3", "bar", "foo", "example")
|
| 22 |
+
.smembers("amazing-set")
|
| 23 |
+
// .evalsha("aijsojiasd", [], [])
|
| 24 |
+
.get("foo")
|
| 25 |
+
.exec();
|
| 26 |
+
|
| 27 |
+
console.log(pipelineResponse);
|
| 28 |
})();
|
lib/srh/http/base_router.ex
CHANGED
|
@@ -2,6 +2,7 @@ defmodule Srh.Http.BaseRouter do
|
|
| 2 |
use Plug.Router
|
| 3 |
alias Srh.Http.RequestValidator
|
| 4 |
alias Srh.Http.CommandHandler
|
|
|
|
| 5 |
|
| 6 |
plug(:match)
|
| 7 |
plug(Plug.Parsers, parsers: [:json], pass: ["application/json"], json_decoder: Jason)
|
|
@@ -12,27 +13,30 @@ defmodule Srh.Http.BaseRouter do
|
|
| 12 |
end
|
| 13 |
|
| 14 |
post "/" do
|
| 15 |
-
conn
|
| 16 |
-
|> handle_extract_auth(&CommandHandler.handle_command(conn, &1))
|
| 17 |
-
|> handle_response(conn)
|
| 18 |
end
|
| 19 |
|
| 20 |
post "/pipeline" do
|
| 21 |
-
conn
|
| 22 |
-
|> handle_extract_auth(&CommandHandler.handle_command_array(conn, &1))
|
| 23 |
-
|> handle_response(conn)
|
| 24 |
end
|
| 25 |
|
| 26 |
post "/multi-exec" do
|
| 27 |
-
conn
|
| 28 |
-
|> handle_extract_auth(&CommandHandler.handle_command_transaction_array(conn, &1))
|
| 29 |
-
|> handle_response(conn)
|
| 30 |
end
|
| 31 |
|
| 32 |
match _ do
|
| 33 |
send_resp(conn, 404, "Endpoint not found")
|
| 34 |
end
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
defp handle_extract_auth(conn, success_lambda) do
|
| 37 |
case conn
|
| 38 |
|> get_req_header("authorization")
|
|
@@ -45,6 +49,24 @@ defmodule Srh.Http.BaseRouter do
|
|
| 45 |
end
|
| 46 |
end
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
defp handle_response(response, conn) do
|
| 49 |
%{code: code, message: message, json: json} =
|
| 50 |
case response do
|
|
|
|
| 2 |
use Plug.Router
|
| 3 |
alias Srh.Http.RequestValidator
|
| 4 |
alias Srh.Http.CommandHandler
|
| 5 |
+
alias Srh.Http.ResultEncoder
|
| 6 |
|
| 7 |
plug(:match)
|
| 8 |
plug(Plug.Parsers, parsers: [:json], pass: ["application/json"], json_decoder: Jason)
|
|
|
|
| 13 |
end
|
| 14 |
|
| 15 |
post "/" do
|
| 16 |
+
do_command_request(conn, &CommandHandler.handle_command(&1, &2))
|
|
|
|
|
|
|
| 17 |
end
|
| 18 |
|
| 19 |
post "/pipeline" do
|
| 20 |
+
do_command_request(conn, &CommandHandler.handle_command_array(&1, &2))
|
|
|
|
|
|
|
| 21 |
end
|
| 22 |
|
| 23 |
post "/multi-exec" do
|
| 24 |
+
do_command_request(conn, &CommandHandler.handle_command_transaction_array(&1, &2))
|
|
|
|
|
|
|
| 25 |
end
|
| 26 |
|
| 27 |
match _ do
|
| 28 |
send_resp(conn, 404, "Endpoint not found")
|
| 29 |
end
|
| 30 |
|
| 31 |
+
defp do_command_request(conn, success_lambda) do
|
| 32 |
+
encoding_enabled = handle_extract_encoding(conn)
|
| 33 |
+
|
| 34 |
+
conn
|
| 35 |
+
|> handle_extract_auth(&success_lambda.(conn, &1))
|
| 36 |
+
|> handle_encoding_step(encoding_enabled)
|
| 37 |
+
|> handle_response(conn)
|
| 38 |
+
end
|
| 39 |
+
|
| 40 |
defp handle_extract_auth(conn, success_lambda) do
|
| 41 |
case conn
|
| 42 |
|> get_req_header("authorization")
|
|
|
|
| 49 |
end
|
| 50 |
end
|
| 51 |
|
| 52 |
+
defp handle_extract_encoding(conn) do
|
| 53 |
+
case conn
|
| 54 |
+
|> get_req_header("upstash-encoding")
|
| 55 |
+
|> RequestValidator.validate_encoding_header() do
|
| 56 |
+
{:ok, _encoding_enabled} -> true
|
| 57 |
+
{:error, _} -> false # it's not required to be present
|
| 58 |
+
end
|
| 59 |
+
end
|
| 60 |
+
|
| 61 |
+
defp handle_encoding_step(response, encoding_enabled) do
|
| 62 |
+
case encoding_enabled do
|
| 63 |
+
true ->
|
| 64 |
+
# We need to use the encoder to
|
| 65 |
+
ResultEncoder.encode_response(response)
|
| 66 |
+
false -> response
|
| 67 |
+
end
|
| 68 |
+
end
|
| 69 |
+
|
| 70 |
defp handle_response(response, conn) do
|
| 71 |
%{code: code, message: message, json: json} =
|
| 72 |
case response do
|
lib/srh/http/request_validator.ex
CHANGED
|
@@ -26,6 +26,21 @@ defmodule Srh.Http.RequestValidator do
|
|
| 26 |
|
| 27 |
defp do_validate_pipeline_item(_), do: :error
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
def validate_bearer_header(header_value_array) when is_list(header_value_array) do
|
| 30 |
do_validate_bearer_header(header_value_array)
|
| 31 |
end
|
|
|
|
| 26 |
|
| 27 |
defp do_validate_pipeline_item(_), do: :error
|
| 28 |
|
| 29 |
+
def validate_encoding_header(header_value_array) when is_list(header_value_array) do
|
| 30 |
+
do_validate_encoding_header(header_value_array)
|
| 31 |
+
end
|
| 32 |
+
|
| 33 |
+
# This has been broken up like this to future-proof different encoding modes in the future
|
| 34 |
+
defp do_validate_encoding_header([first_item | rest]) do
|
| 35 |
+
case first_item do
|
| 36 |
+
"base64" -> {:ok, true}
|
| 37 |
+
|
| 38 |
+
_ -> do_validate_encoding_header(rest)
|
| 39 |
+
end
|
| 40 |
+
end
|
| 41 |
+
|
| 42 |
+
defp do_validate_encoding_header([]), do: {:error, :not_found}
|
| 43 |
+
|
| 44 |
def validate_bearer_header(header_value_array) when is_list(header_value_array) do
|
| 45 |
do_validate_bearer_header(header_value_array)
|
| 46 |
end
|
lib/srh/http/result_encoder.ex
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Srh.Http.ResultEncoder do
|
| 2 |
+
|
| 3 |
+
# Errors don't get encoded, we need to skip over those
|
| 4 |
+
def encode_response({:redis_error, error_result_map}) do
|
| 5 |
+
{:redis_error, error_result_map}
|
| 6 |
+
end
|
| 7 |
+
|
| 8 |
+
# List-based responses, they will contain multiple entries
|
| 9 |
+
# It's important to note that this is DIFFERENT from a list of values,
|
| 10 |
+
# as it's a list of separate command responses. Each is a map that either
|
| 11 |
+
# Contains a result or an error
|
| 12 |
+
def encode_response({:ok, result_list}) when is_list(result_list) do
|
| 13 |
+
# Each one of these entries needs to be encoded
|
| 14 |
+
{:ok, encode_response_list(result_list, [])}
|
| 15 |
+
end
|
| 16 |
+
|
| 17 |
+
# Single item response
|
| 18 |
+
def encode_response({:ok, %{result: result_value}}) do
|
| 19 |
+
{:ok, %{result: encode_result_value(result_value)}}
|
| 20 |
+
end
|
| 21 |
+
|
| 22 |
+
## RESULT LIST ENCODING ##
|
| 23 |
+
|
| 24 |
+
defp encode_response_list([current | rest], encoded_responses) do
|
| 25 |
+
encoded_current_entry = case current do
|
| 26 |
+
%{result: value} ->
|
| 27 |
+
%{result: encode_result_value(value)} # Encode the value
|
| 28 |
+
%{error: error_message} ->
|
| 29 |
+
%{error: error_message} # We don't encode errors
|
| 30 |
+
end
|
| 31 |
+
|
| 32 |
+
encode_response_list(rest, [encoded_current_entry | encoded_responses])
|
| 33 |
+
end
|
| 34 |
+
|
| 35 |
+
defp encode_response_list([], encoded_responses) do
|
| 36 |
+
Enum.reverse(encoded_responses)
|
| 37 |
+
end
|
| 38 |
+
|
| 39 |
+
## RESULT VALUE ENCODING ##
|
| 40 |
+
|
| 41 |
+
# Numbers are ignored
|
| 42 |
+
defp encode_result_value(value) when is_number(value), do: value
|
| 43 |
+
|
| 44 |
+
# Null/nil is ignored
|
| 45 |
+
defp encode_result_value(value) when is_nil(value), do: value
|
| 46 |
+
|
| 47 |
+
# Strings / blobs (any binary data) is encoded to Base64
|
| 48 |
+
defp encode_result_value(value) when is_binary(value), do: Base.encode64(value)
|
| 49 |
+
|
| 50 |
+
defp encode_result_value(arr) when is_list(arr) do
|
| 51 |
+
encode_result_value_list(arr, [])
|
| 52 |
+
end
|
| 53 |
+
|
| 54 |
+
## RESULT VALUE LIST ENCODING ##
|
| 55 |
+
|
| 56 |
+
# Arrays can have values that are encoded, or aren't, based on whats laid out above
|
| 57 |
+
defp encode_result_value_list([current | rest], encoded_responses) do
|
| 58 |
+
encoded_value = encode_result_value(current)
|
| 59 |
+
encode_result_value_list(rest, [encoded_value | encoded_responses])
|
| 60 |
+
end
|
| 61 |
+
|
| 62 |
+
defp encode_result_value_list([], encoded_responses) do
|
| 63 |
+
# There are no responses left, and since we add them backwards, we need to flip the list
|
| 64 |
+
Enum.reverse(encoded_responses)
|
| 65 |
+
end
|
| 66 |
+
end
|