Add getters and versioned API to `DynamicJSEndpointRegistry` (#6234)

This commit is contained in:
Eddy Ashton 2024-06-07 17:10:19 +01:00 коммит произвёл GitHub
Родитель 3ba5e0133b
Коммит e7464e42d9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
5 изменённых файлов: 349 добавлений и 68 удалений

Просмотреть файл

@ -12,8 +12,9 @@ namespace ccf::js
{
struct Metadata
{
// Path -> {HTTP Method -> Properties}
std::map<
std::string,
ccf::endpoints::URI,
std::map<std::string, ccf::endpoints::EndpointProperties>>
endpoints;
};

Просмотреть файл

@ -76,8 +76,32 @@ namespace ccf::js
* Call this to populate the KV with JS endpoint definitions, so they can
* later be dispatched to.
*/
void install_custom_endpoints(
ccf::endpoints::EndpointContext& ctx, const ccf::js::Bundle& bundle);
ccf::ApiResult install_custom_endpoints_v1(
kv::Tx& tx, const ccf::js::Bundle& bundle);
/**
* Retrieve all endpoint definitions currently in-use. This returns the same
* bundle written by a recent call to install_custom_endpoints. Note that
* some values (module paths, casing of HTTP methods) may differ slightly
* due to internal normalisation.
*/
ccf::ApiResult get_custom_endpoints_v1(
ccf::js::Bundle& bundle, kv::ReadOnlyTx& tx);
/**
* Retrieve property definition for a single JS endpoint.
*/
ccf::ApiResult get_custom_endpoint_properties_v1(
ccf::endpoints::EndpointProperties& properties,
kv::ReadOnlyTx& tx,
const ccf::RESTVerb& verb,
const ccf::endpoints::URI& uri);
/**
* Retrieve content of a single JS module.
*/
ccf::ApiResult get_custom_endpoint_module_v1(
std::string& code, kv::ReadOnlyTx& tx, const std::string& module_name);
/// \defgroup Overrides for base EndpointRegistry functions, looking up JS
/// endpoints before delegating to base implementation.

Просмотреть файл

@ -156,7 +156,18 @@ namespace basicapp
caller_identity.content.begin(), caller_identity.content.end());
const auto wrapper = j.get<ccf::js::Bundle>();
install_custom_endpoints(ctx, wrapper);
result = install_custom_endpoints_v1(ctx.tx, wrapper);
if (result != ccf::ApiResult::OK)
{
ctx.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
fmt::format(
"Failed to install endpoints: {}",
ccf::api_result_to_str(result)));
return;
}
ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT);
};
@ -167,6 +178,83 @@ namespace basicapp
{ccf::user_cose_sign1_auth_policy})
.set_auto_schema<ccf::js::Bundle, void>()
.install();
auto get_custom_endpoints = [this](ccf::endpoints::EndpointContext& ctx) {
ccf::js::Bundle bundle;
auto result = get_custom_endpoints_v1(bundle, ctx.tx);
if (result != ccf::ApiResult::OK)
{
ctx.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
fmt::format(
"Failed to get endpoints: {}", ccf::api_result_to_str(result)));
return;
}
ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
ctx.rpc_ctx->set_response_header(
http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON);
ctx.rpc_ctx->set_response_body(nlohmann::json(bundle).dump(2));
};
make_endpoint(
"/custom_endpoints",
HTTP_GET,
get_custom_endpoints,
{ccf::empty_auth_policy})
.set_auto_schema<void, ccf::js::Bundle>()
.install();
auto get_custom_endpoints_module =
[this](ccf::endpoints::EndpointContext& ctx) {
std::string module_name;
{
const auto parsed_query =
http::parse_query(ctx.rpc_ctx->get_request_query());
std::string error;
if (!http::get_query_value(
parsed_query, "module_name", module_name, error))
{
ctx.rpc_ctx->set_error(
HTTP_STATUS_BAD_REQUEST,
ccf::errors::InvalidQueryParameterValue,
std::move(error));
return;
}
}
std::string code;
auto result =
get_custom_endpoint_module_v1(code, ctx.tx, module_name);
if (result != ccf::ApiResult::OK)
{
ctx.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
fmt::format(
"Failed to get module: {}", ccf::api_result_to_str(result)));
return;
}
ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
ctx.rpc_ctx->set_response_header(
http::headers::CONTENT_TYPE,
http::headervalues::contenttype::JAVASCRIPT);
ctx.rpc_ctx->set_response_body(std::move(code));
};
make_endpoint(
"/custom_endpoints/modules",
HTTP_GET,
get_custom_endpoints_module,
{ccf::empty_auth_policy})
.add_query_parameter<std::string>("module_name")
.install();
}
};
}

Просмотреть файл

@ -37,6 +37,16 @@
namespace ccf::js
{
std::string normalised_module_path(std::string_view sv)
{
if (!sv.starts_with("/"))
{
return fmt::format("/{}", sv);
}
return std::string(sv);
}
void DynamicJSEndpointRegistry::do_execute_request(
const CustomJSEndpoint* endpoint,
ccf::endpoints::EndpointContext& endpoint_ctx,
@ -450,74 +460,183 @@ namespace ccf::js
});
}
void DynamicJSEndpointRegistry::install_custom_endpoints(
ccf::endpoints::EndpointContext& ctx, const ccf::js::Bundle& bundle)
ccf::ApiResult DynamicJSEndpointRegistry::install_custom_endpoints_v1(
kv::Tx& tx, const ccf::js::Bundle& bundle)
{
auto endpoints =
ctx.tx.template rw<ccf::endpoints::EndpointsMap>(metadata_map);
endpoints->clear();
for (const auto& [url, methods] : bundle.metadata.endpoints)
try
{
for (const auto& [method, metadata] : methods)
auto endpoints =
tx.template rw<ccf::endpoints::EndpointsMap>(metadata_map);
endpoints->clear();
for (const auto& [url, methods] : bundle.metadata.endpoints)
{
std::string method_upper = method;
nonstd::to_upper(method_upper);
const auto key = ccf::endpoints::EndpointKey{url, method_upper};
endpoints->put(key, metadata);
}
}
auto modules = ctx.tx.template rw<ccf::Modules>(modules_map);
modules->clear();
for (const auto& module_def : bundle.modules)
{
modules->put(fmt::format("/{}", module_def.name), module_def.module);
}
// Trigger interpreter flush, in case interpreter reuse
// is enabled for some endpoints
auto interpreter_flush =
ctx.tx.template rw<ccf::InterpreterFlush>(interpreter_flush_map);
interpreter_flush->put(true);
// Refresh app bytecode
ccf::js::core::Context jsctx(ccf::js::TxAccess::APP_RW);
jsctx.runtime().set_runtime_options(
ctx.tx.ro<ccf::JSEngine>(runtime_options_map)->get(),
ccf::js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);
auto quickjs_version =
ctx.tx.wo<ccf::ModulesQuickJsVersion>(modules_quickjs_version_map);
auto quickjs_bytecode =
ctx.tx.wo<ccf::ModulesQuickJsBytecode>(modules_quickjs_bytecode_map);
quickjs_version->put(ccf::quickjs_version);
quickjs_bytecode->clear();
jsctx.set_module_loader(
std::make_shared<ccf::js::modules::KvModuleLoader>(modules));
modules->foreach([&](const auto& name, const auto& src) {
auto module_val = jsctx.eval(
src.c_str(),
src.size(),
name.c_str(),
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
uint8_t* out_buf;
size_t out_buf_len;
int flags = JS_WRITE_OBJ_BYTECODE;
out_buf = JS_WriteObject(jsctx, &out_buf_len, module_val.val, flags);
if (!out_buf)
{
throw std::runtime_error(
fmt::format("Unable to serialize bytecode for JS module '{}'", name));
for (const auto& [method, metadata] : methods)
{
std::string method_upper = method;
nonstd::to_upper(method_upper);
const auto key = ccf::endpoints::EndpointKey{url, method_upper};
endpoints->put(key, metadata);
}
}
quickjs_bytecode->put(name, {out_buf, out_buf + out_buf_len});
js_free(jsctx, out_buf);
auto modules = tx.template rw<ccf::Modules>(modules_map);
modules->clear();
for (const auto& moduledef : bundle.modules)
{
modules->put(normalised_module_path(moduledef.name), moduledef.module);
}
return true;
});
// Trigger interpreter flush, in case interpreter reuse
// is enabled for some endpoints
auto interpreter_flush =
tx.template rw<ccf::InterpreterFlush>(interpreter_flush_map);
interpreter_flush->put(true);
// Refresh app bytecode
ccf::js::core::Context jsctx(ccf::js::TxAccess::APP_RW);
jsctx.runtime().set_runtime_options(
tx.ro<ccf::JSEngine>(runtime_options_map)->get(),
ccf::js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);
auto quickjs_version =
tx.wo<ccf::ModulesQuickJsVersion>(modules_quickjs_version_map);
auto quickjs_bytecode =
tx.wo<ccf::ModulesQuickJsBytecode>(modules_quickjs_bytecode_map);
quickjs_version->put(ccf::quickjs_version);
quickjs_bytecode->clear();
jsctx.set_module_loader(
std::make_shared<ccf::js::modules::KvModuleLoader>(modules));
modules->foreach([&](const auto& name, const auto& src) {
auto module_val = jsctx.eval(
src.c_str(),
src.size(),
name.c_str(),
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
uint8_t* out_buf;
size_t out_buf_len;
int flags = JS_WRITE_OBJ_BYTECODE;
out_buf = JS_WriteObject(jsctx, &out_buf_len, module_val.val, flags);
if (!out_buf)
{
throw std::runtime_error(fmt::format(
"Unable to serialize bytecode for JS module '{}'", name));
}
quickjs_bytecode->put(name, {out_buf, out_buf + out_buf_len});
js_free(jsctx, out_buf);
return true;
});
return ccf::ApiResult::OK;
}
catch (const std::exception& e)
{
LOG_FAIL_FMT("{}", e.what());
return ApiResult::InternalError;
}
}
ccf::ApiResult DynamicJSEndpointRegistry::get_custom_endpoints_v1(
ccf::js::Bundle& bundle, kv::ReadOnlyTx& tx)
{
try
{
auto endpoints_handle =
tx.template ro<ccf::endpoints::EndpointsMap>(metadata_map);
endpoints_handle->foreach([&endpoints = bundle.metadata.endpoints](
const auto& endpoint_key,
const auto& properties) {
using PropertiesMap =
std::map<std::string, ccf::endpoints::EndpointProperties>;
auto it = endpoints.find(endpoint_key.uri_path);
if (it == endpoints.end())
{
it =
endpoints.emplace_hint(it, endpoint_key.uri_path, PropertiesMap{});
}
PropertiesMap& method_properties = it->second;
method_properties.emplace_hint(
method_properties.end(), endpoint_key.verb.c_str(), properties);
return true;
});
auto modules_handle = tx.template ro<ccf::Modules>(modules_map);
modules_handle->foreach(
[&modules =
bundle.modules](const auto& module_name, const auto& module_src) {
modules.push_back({module_name, module_src});
return true;
});
return ApiResult::OK;
}
catch (const std::exception& e)
{
LOG_FAIL_FMT("{}", e.what());
return ApiResult::InternalError;
}
}
ccf::ApiResult DynamicJSEndpointRegistry::get_custom_endpoint_properties_v1(
ccf::endpoints::EndpointProperties& properties,
kv::ReadOnlyTx& tx,
const ccf::RESTVerb& verb,
const ccf::endpoints::URI& uri)
{
try
{
auto endpoints = tx.ro<ccf::endpoints::EndpointsMap>(metadata_map);
const auto key = ccf::endpoints::EndpointKey{uri, verb};
auto it = endpoints->get(key);
if (it.has_value())
{
properties = it.value();
return ApiResult::OK;
}
else
{
return ApiResult::NotFound;
}
}
catch (const std::exception& e)
{
LOG_FAIL_FMT("{}", e.what());
return ApiResult::InternalError;
}
}
ccf::ApiResult DynamicJSEndpointRegistry::get_custom_endpoint_module_v1(
std::string& code, kv::ReadOnlyTx& tx, const std::string& module_name)
{
try
{
auto modules = tx.template ro<ccf::Modules>(modules_map);
auto it = modules->get(normalised_module_path(module_name));
if (it.has_value())
{
code = it.value();
return ApiResult::OK;
}
else
{
return ApiResult::NotFound;
}
}
catch (const std::exception& e)
{
LOG_FAIL_FMT("{}", e.what());
return ApiResult::InternalError;
}
}
ccf::endpoints::EndpointDefinitionPtr DynamicJSEndpointRegistry::

Просмотреть файл

@ -16,16 +16,24 @@ import npm_tests
from loguru import logger as LOG
TESTJS = """
import { foo } from "./bar/baz.js";
export function content(request) {
return {
statusCode: 200,
body: {
payload: "Test content",
payload: foo(),
},
};
}
"""
FOOJS = """
export function foo() {
return "Test content";
}
"""
TESTJS_ROLE = """
export function content(request) {
let raw_id = ccf.strToBuf(request.caller.id);
@ -77,7 +85,10 @@ def test_custom_endpoints(network, args):
}
}
modules = [{"name": "test.js", "module": TESTJS}]
modules = [
{"name": "test.js", "module": TESTJS},
{"name": "bar/baz.js", "module": FOOJS},
]
bundle_with_content = {
"metadata": {"endpoints": {"/content": content_endpoint_def}},
@ -89,10 +100,46 @@ def test_custom_endpoints(network, args):
"modules": modules,
}
def upper_cased_keys(obj):
return {k.upper(): v for k, v in obj.items()}
def prefixed_module_name(module_def):
if module_def["name"].startswith("/"):
return module_def
else:
return {**module_def, "name": f"/{module_def['name']}"}
def same_modulo_normalisation(expected, actual):
# Normalise expected (in the same way that CCF will) so we can do direct comparison
expected["metadata"]["endpoints"] = {
path: upper_cased_keys(op)
for path, op in expected["metadata"]["endpoints"].items()
}
expected["modules"] = [
prefixed_module_name(module_def) for module_def in expected["modules"]
]
return expected == actual
def test_getters(c, expected_body):
r = c.get("/app/custom_endpoints")
assert r.status_code == http.HTTPStatus.OK, r
assert same_modulo_normalisation(
expected_body, r.body.json()
), f"Expected:\n{expected_body}\n\n\nActual:\n{r.body.json()}"
for module_def in modules:
r = c.get(f"/app/custom_endpoints/modules?module_name={module_def['name']}")
assert r.status_code == http.HTTPStatus.OK, r
assert (
r.body.text() == module_def["module"]
), f"Expected:\n{module_def['module']}\n\n\nActual:\n{r.body.text()}"
with primary.client(None, None, user.local_id) as c:
r = c.put("/app/custom_endpoints", body=bundle_with_content)
assert r.status_code == http.HTTPStatus.NO_CONTENT.value, r.status_code
test_getters(c, bundle_with_content)
with primary.client() as c:
r = c.get("/app/not_content")
assert r.status_code == http.HTTPStatus.NOT_FOUND.value, r.status_code
@ -105,6 +152,8 @@ def test_custom_endpoints(network, args):
r = c.put("/app/custom_endpoints", body=bundle_with_other_content)
assert r.status_code == http.HTTPStatus.NO_CONTENT.value, r.status_code
test_getters(c, bundle_with_other_content)
with primary.client() as c:
r = c.get("/app/other_content")
assert r.status_code == http.HTTPStatus.OK.value, r.status_code