зеркало из https://github.com/microsoft/CCF.git
Add getters and versioned API to `DynamicJSEndpointRegistry` (#6234)
This commit is contained in:
Родитель
3ba5e0133b
Коммит
e7464e42d9
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче