Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tsoa-based TypeScript app #1614

Merged
merged 13 commits into from
Sep 18, 2020
67 changes: 67 additions & 0 deletions tests/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import http
import subprocess
import os
import json
import infra.network
import infra.path
import infra.proc
Expand Down Expand Up @@ -205,6 +206,71 @@ def test_npm_app(network, args):
assert r.status_code == http.HTTPStatus.OK, r.status_code
assert r.body["available"], r.body

return network


@reqs.description("Test tsoa-based Node.js/npm app")
def test_npm_tsoa_app(network, args):
primary, _ = network.find_nodes()

LOG.info("Building npm app")
app_dir = os.path.join(THIS_DIR, "npm-tsoa-app")
subprocess.run(["npm", "ci"], cwd=app_dir, check=True)
subprocess.run(["npm", "run", "build"], cwd=app_dir, check=True)

LOG.info("Deploying npm app modules")
module_name_prefix = "/my-tsoa-npm-app/"
dist_dir = os.path.join(app_dir, "dist")

proposal_body, _ = ccf.proposal_generator.update_modules(
module_name_prefix, dist_dir
)
proposal = network.consortium.get_any_active_member().propose(
primary, proposal_body
)
network.consortium.vote_using_majority(primary, proposal)

LOG.info("Deploying endpoint script")
metadata_path = os.path.join(dist_dir, "endpoints.json")
with open(metadata_path) as f:
metadata = json.load(f)
# Temporarily only until endpoints can be called directly without proxy.
app_script = "return {"
for url, methods in metadata["endpoints"].items():
for method, cfg in methods.items():
app_script += f"""
["{method.upper()} {url[1:]}"] = [[
import {{ {cfg["js_function"]} as f }}
from ".{module_name_prefix}{cfg["js_module"]}";
export default (request) => f(request);
]],"""
app_script = app_script[:-1] + "\n}"

with tempfile.NamedTemporaryFile("w") as f:
f.write(app_script)
f.flush()
network.consortium.set_js_app(remote_node=primary, app_script_path=f.name)

LOG.info("Calling npm app endpoints")
with primary.client("user0") as c:
body = [1, 2, 3, 4]
r = c.post("/app/partition", body)
assert r.status_code == http.HTTPStatus.OK, r.status_code
assert r.body == [[1, 3], [2, 4]], r.body

r = c.post("/app/proto", body)
assert r.status_code == http.HTTPStatus.OK, r.status_code
assert r.headers["content-type"] == "application/x-protobuf"
# We could now decode the protobuf message but given all the machinery
achamayou marked this conversation as resolved.
Show resolved Hide resolved
# involved to make it happen (code generation with protoc) we'll leave it at that.
assert len(r.body) == 14, len(r.body)

r = c.get("/app/crypto")
assert r.status_code == http.HTTPStatus.OK, r.status_code
assert r.body["available"], r.body

return network


def run(args):
hosts = ["localhost"] * (3 if args.consensus == "bft" else 2)
Expand All @@ -217,6 +283,7 @@ def run(args):
network = test_modules_remove(network, args)
network = test_module_import(network, args)
network = test_npm_app(network, args)
network = test_npm_tsoa_app(network, args)


if __name__ == "__main__":
Expand Down
3 changes: 3 additions & 0 deletions tests/npm-tsoa-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
build/
19 changes: 19 additions & 0 deletions tests/npm-tsoa-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Node.js/npm CCF app using tsoa
achamayou marked this conversation as resolved.
Show resolved Hide resolved
letmaik marked this conversation as resolved.
Show resolved Hide resolved

This folder contains a sample CCF app written in TypeScript with [tsoa](https://tsoa-community.github.io/docs/).
See the [README.md](../npm-app/README.md) file of the `npm-app` folder for a general introduction to CCF's JavaScript environment and npm.

tsoa generates OpenAPI definitions from TypeScript types and JSDoc annotations.
It also uses the generated schemas to validate the request data.

Note that tsoa currently focuses on JSON as content type.
Using other content types is possible but requires to manually specify the OpenAPI definition.
See [`proto.ts`](src/controllers/proto.ts) for details.

Additional CCF-specific metadata is specified in `endpoints.json`.
When new endpoints are added in the TypeScript code, then those get automatically added to `endpoints.json`
when running `npm run build`.
Note that most of the metadata is still unused as support for it is missing in CCF for JS apps.
This will be addressed in the near future.

See the `modules.py` test for how this app is built and deployed to CCF.
40 changes: 40 additions & 0 deletions tests/npm-tsoa-app/demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>

<script>
class CCFBody {
constructor(body) {
this.body = body // string
}
text() {
return this.body;
}
json() {
return JSON.parse(this.body);
}
arrayBuffer() {
return new TextEncoder().encode(this.body).buffer;
}
}
</script>

<script type="module">
import {computePartition} from "../dist/build/PartitionControllerProxy.js";
import {wrapInProtobuf} from "../dist/build/ProtoControllerProxy.js";
import {getCrypto} from "../dist/build/CryptoControllerProxy.js";

const request = {
headers: {},
params: {},
body: new CCFBody("[1,2,3,4]"),
query: "foo=bar"
}

console.log('Invoking partition()');
console.log(computePartition(request));
console.log('Invoking proto()');
console.log(wrapInProtobuf(request));
console.log('Invoking crypto()');
console.log(getCrypto(request));
</script>

Open your developer console!
52 changes: 52 additions & 0 deletions tests/npm-tsoa-app/endpoints.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"endpoints": {
"/crypto": {
"get": {
"js_module": "build/CryptoControllerProxy.js",
"js_function": "getCrypto",
"forwarding_required": "always",
"execute_locally": false,
"require_client_signature": false,
"require_client_identity": true,
"readonly": true
}
},
"/partition": {
"post": {
"js_module": "build/PartitionControllerProxy.js",
"js_function": "computePartition",
"forwarding_required": "always",
"execute_locally": false,
"require_client_signature": false,
"require_client_identity": true,
"readonly": false
}
},
"/proto": {
"post": {
"js_module": "build/ProtoControllerProxy.js",
"js_function": "wrapInProtobuf",
"forwarding_required": "always",
"execute_locally": false,
"require_client_signature": false,
"require_client_identity": true,
"readonly": false,
"openapi": {
"requestBody": {
"required": true,
"content": {
"text/plain": {}
}
},
"responses": {
"200": {
"content": {
"application/x-protobuf": {}
}
}
}
}
}
}
}
}
Loading