Web Services
- Routing
- Registration
- Interfaces
- Helpers
Routing
psinode passes most HTTP requests to the SystemService::ProxySys service, which then routes requests to the appropriate service's serveSys action (see diagram). The services run in RPC mode; this prevents them from writing to the database, but allows them to read data they normally can't. See psibase::DbId.
SystemService::CommonSys provides services common to all domains under the /common tree. It also serves the chain's main page.
SystemService::PsiSpaceSys provides web hosting to non-service accounts.
psinode directly handles requests which start with /native, e.g. /native/push_transaction. Services don't serve these.
Registration
Services which wish to serve HTTP requests need to register using the SystemService::ProxySys service's SystemService::ProxySys::registerServer action. There are multiple ways to do this:
psibase deployhas a--register-proxyoption (shortcut-p) that can do this while deploying the service.psibase register-proxycan also do it. TODO: implementpsibase register-proxy.- A service may call
registerServerduring its own initialization action.
A service doesn't have to serve HTTP requests itself; it may delegate this to another service during registration.
Interfaces
Services which serve HTTP implement these interfaces:
- psibase::ServerInterface (required)
- psibase::StorageInterface (optional)
psibase::ServerInterface
struct psibase::ServerInterface {
serveSys(...); // Handle HTTP requests
};
Interface for services which serve http.
proxy.sys uses this interface to call into services to respond to http requests.
Do not inherit from this. To implement this interface, add a serveSys action to your service and reflect it.
psibase::ServerInterface::serveSys
std::optional<HttpReply> psibase::ServerInterface::serveSys(
psibase::HttpRequest request
);
Handle HTTP requests.
Define this action in your service to handle HTTP requests. You'll also need to register your service.
serveSys can do any of the following:
- Return
std::nulloptto signal not found. psinode produces a 404 response in this case. - Abort. psinode produces a 500 response with the service's abort message.
- Return a psibase::HttpReply. psinode produces a 200 response with the body and contentType returned.
- Call other services.
A service runs in RPC mode while serving an HTTP request. This mode prevents database writes, but allows database reads, including reading data and events which are normally not available to services; see psibase::DbId.
psibase::HttpRequest
struct psibase::HttpRequest {
std::string host; // Fully-qualified domain name
std::string rootHost; // host, but without service subdomain
std::string method; // "GET" or "POST"
std::string target; // Absolute path, e.g. "/index.js"
std::string contentType; // "application/json", "text/html", ...
std::vector<char> body; // Request body, e.g. POST data
};
An HTTP Request.
Most services receive this via their serveSys action.
SystemService::ProxySys receives it via its serve exported function.
psibase::HttpReply
struct psibase::HttpReply {
std::string contentType; // "application/json", "text/html", ...
std::vector<char> body; // Response body
std::vector<HttpHeader> headers; // HTTP Headers
};
An HTTP reply.
Services return this from their serveSys action.
psibase::StorageInterface
struct psibase::StorageInterface {
storeSys(...); // Store a file
};
Interface for services which support storing files.
Some services support storing files which they then serve via HTTP. This is the standard interface for these services.
Do not inherit from this. To implement this interface, add a storeSys action to your service and reflect it.
psibase::StorageInterface::storeSys
void psibase::StorageInterface::storeSys(
std::string_view path,
std::string_view contentType,
std::vector<char> content
);
Store a file.
Define this action in your service to handle file storage requests. This action should store the file in the service's tables. The service can then serve these files via HTTP.
path: absolute path to file. e.g./index.htmlfor the main pagecontentType:text/html,text/javascript,application/octet-stream, ...content: file content
The psibase upload command uses this action.
storeContent simplifies implementing this.
Helpers
These help implement basic functionality:
- psibase::serveSimpleUI
- psibase::serveActionTemplates
- psibase::servePackAction
- psibase::WebContentRow
- psibase::storeContent
- psibase::serveContent
- psibase::serveGraphQL
- psibase::makeConnection
- psibase::EventDecoder
- psibase::EventQuery
- psibase::makeEventConnection
Here's a common pattern for using these functions:
std::optional<psibase::HttpReply> serveSys(psibase::HttpRequest request)
{
if (auto result = psibase::serveActionTemplates<ExampleService>(request))
return result;
if (auto result = psibase::servePackAction<ExampleService>(request))
return result;
if (request.method == "GET" && request.target == "/")
{
static const char helloWorld[] = "Hello World";
return psibase::HttpReply{
.contentType = "text/plain",
.body = {helloWorld, helloWorld + strlen(helloWorld)},
};
}
return std::nullopt;
}
psibase::serveSimpleUI
template<typename Service, bool IncludeRoot>
std::optional<HttpReply> psibase::serveSimpleUI(
const psibase::HttpRequest & request
);
Serve a developer UI.
This function serves a simple developer UI to help get you started. The UI it generates is not suitable for end users.
This serves the following:
GET /action_templates: provided by serveActionTemplatesPOST /pack_action/x: provided by servePackActionGET /, but only ifIncludeRootis set. This returns the following HTML body:
<html>
<div id="root" class="ui container"></div>
<script src="/common/SimpleUI.mjs" type="module"></script>
</html>
psibase::serveActionTemplates
template<typename Service>
std::optional<HttpReply> psibase::serveActionTemplates(
const psibase::HttpRequest & request
);
Handle /action_templates request.
If request is a GET to /action_templates, then this returns a
JSON object containing a field for each action in Service. The
field names match the action names. The field values are objects
with the action arguments, each containing sample data.
If request doesn't match the above, then this returns std::nullopt.
psibase::servePackAction
template<typename Service>
std::optional<HttpReply> psibase::servePackAction(
const psibase::HttpRequest & request
);
Handle /pack_action/ request.
If request is a POST to /pack_action/x, where x is an action
on Service, then this parses a JSON object containing the arguments
to x, packs them using frackpac, and returns the result as an
application/octet-stream.
If request doesn't match the above, or the action name is not found,
then this returns std::nullopt.
psibase::WebContentRow
struct psibase::WebContentRow {
std::string path; // Absolute path to content, e.g. "/index.mjs"
std::string contentType; // "text/html", "text/javascript", ...
std::vector<char> content; // Content body
};
Content for serving over HTTP.
This the table row format for services which store and serve HTTP files using storeContent and serveContent.
Also includes this definition:
using WebContentTable = Table<WebContentRow, &WebContentRow::path>;
psibase::storeContent
template<typename ...Tables>
void psibase::storeContent(
std::string && path,
std::string && contentType,
std::vector<char> && content,
const ServiceTables<Tables...> & tables
);
Store web content in table.
This stores web content into a service's WebContentTable.
serveContent serves this content via HTTP.
Example use:
// Don't forget to include your service's other tables in this!
using Tables = psibase::ServiceTables<psibase::WebContentTable>;
void MyService::storeSys(
std::string path, std::string contentType, std::vector<char> content)
{
psibase::check(getSender() == getReceiver(), "wrong sender");
psibase::storeContent(std::move(path), std::move(contentType), std::move(content),
Tables{getReceiver()});
}
psibase::serveContent
template<typename ...Tables>
std::optional<HttpReply> psibase::serveContent(
const psibase::HttpRequest & request,
const ServiceTables<Tables...> & tables
);
Serve files via HTTP.
This serves files stored by storeContent.
Example use:
// Don't forget to include your service's other tables in this!
using Tables = psibase::ServiceTables<psibase::WebContentTable>;
std::optional<psibase::HttpReply> MyService::serveSys(
psibase::HttpRequest request)
{
if (auto result = psibase::serveContent(request, Tables{getReceiver()}))
return result;
return std::nullopt;
}
psibase::serveGraphQL
template<typename QueryRoot>
std::optional<HttpReply> psibase::serveGraphQL(
const psibase::HttpRequest & request,
const QueryRoot & queryRoot
);
Handle /graphql request.
This handles graphql requests, including fetching the schema.
GET /graphql: Returns the schema.GET /graphql?query=...: Run query in URL and return JSON result.POST /graphql?query=...: Run query in URL and return JSON result.POST /graphqlwithContent-Type = application/graphql: Run query that's in body and return JSON result.POST /graphqlwithContent-Type = application/json: Body contains a JSON object of the form{"query"="..."}. Run query and return JSON result.
queryRoot should be a reflected object; this shows up in GraphQL as the root
Query type. GraphQL exposes both fields and const methods. Fields may be
any reflected struct. Const methods may return any reflected struct. They should
return objects by value.
psibase::makeConnection
template<typename Connection, typename T, typename Key>
Connection psibase::makeConnection(
const TableIndex<T, Key> & index,
const std::optional<Key> & gt,
const std::optional<Key> & ge,
const std::optional<Key> & lt,
const std::optional<Key> & le,
std::optional<uint32_t> first,
std::optional<uint32_t> last,
const std::optional<std::string> & before,
const std::optional<std::string> & after
);
GraphQL Pagination through TableIndex.
You rarely need to call this directly; see the example.
Template arguments:
Connection: ConnectionT: Type stored in indexKey: Key in index
Arguments:
index: TableIndex to paginate throughgt: Restrict range to keys greater than thisge: Restrict range to keys greater than or equal to thislt: Restrict range to keys less than to thisle: Restrict range to keys less than or equal to thisfirst: Stop after including this many items at the beginning of the rangelast: Stop after including this many items at the end of the rangebefore: Opaque cursor value. Resume paging; include keys before this pointafter: Opaque cursor value. Resume paging; include keys after this point
By default, makeConnection pages through the entire index's range.
gt, ge, lt, and le restrict the range. They can be used in any
combination (set intersection).
first, last, before, after, PageInfo, Connection, and
Edge match the GraphQL Cursor Connections Specification
(aka GraphQL Pagination). first and after page through the
range defined above in the forward direction. last and before
page through the range defined above in the reverse direction.
Combining first with last isn't recommended, but matches the
behavior in the specification.
makeConnection example
This demonstrates exposing a table's contents via GraphQL. This example doesn't include a way to fill the table; that's left as an exercise to the reader. Hint: service-based RPC and GraphQL only support read-only operations; you must use actions to write to a table.
#include <psibase/Service.hpp>
#include <psibase/dispatch.hpp>
#include <psibase/serveGraphQL.hpp>
#include <psibase/serveSimpleUI.hpp>
struct MyType
{
uint32_t primaryKey;
std::string secondaryKey;
std::string moreData;
std::string someFn(std::string arg1, std::string arg2) const
{
return arg1 + secondaryKey + arg2 + moreData;
}
};
PSIO_REFLECT(MyType,
primaryKey, secondaryKey, moreData,
method(someFn, arg1, arg2))
using MyTable = psibase::Table<
MyType, &MyType::primaryKey, &MyType::secondaryKey>;
using MyTables = psibase::ServiceTables<MyTable>;
struct Query
{
psibase::AccountNumber service;
auto rowsByPrimary() const {
return MyTables{service}.open<MyTable>().getIndex<0>();
}
auto rowsBySecondary() const {
return MyTables{service}.open<MyTable>().getIndex<1>();
}
};
PSIO_REFLECT(Query, method(rowsByPrimary), method(rowsBySecondary))
struct ExampleService : psibase::Service<ExampleService>
{
std::optional<psibase::HttpReply> serveSys(psibase::HttpRequest request)
{
if (auto result = psibase::serveSimpleUI<ExampleService, true>(request))
return result;
if (auto result = psibase::serveGraphQL(request, Query{getReceiver()}))
return result;
return std::nullopt;
}
};
PSIO_REFLECT(ExampleService, method(serveSys, request))
PSIBASE_DISPATCH(ExampleService)
This example doesn't call makeConnection directly; it's automatic.
If a member function on a query object:
- is
const, and - is reflected, and
- has no arguments, and
- returns a TableIndex
then the system exposes that function to GraphQL as a function which
takes gt, ge, lt, le, first, last, before, and after.
The system calls makeConnection automatically.
serveGraphQL generates this GraphQL schema and processes queries which conform to it:
type MyType {
primaryKey: Float!
secondaryKey: String!
moreData: String!
someFn(arg1: String! arg2: String!): String!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String!
endCursor: String!
}
type MyTypeEdge {
node: MyType!
cursor: String!
}
type MyTypeConnection {
edges: [MyTypeEdge!]!
pageInfo: PageInfo!
}
type Query {
rowsByPrimary(
gt: Float ge: Float lt: Float le: Float
first: Float last: Float
before: String after: String): MyTypeConnection!
rowsBySecondary(
gt: String ge: String lt: String le: String
first: Float last: Float
before: String after: String): MyTypeConnection!
}
Things of note:
rowsByPrimaryandrowsBySecondaryautomatically havemakeConnection's arguments.MyTypeEdgeandMyTypeConnectionare automatically generated fromMyType.- Returned rows (
MyType) include MyType's fields and thesomeFnmethod. Onlyconstmethods are exposed. - serveGraphQL automatically chooses GraphQL types which cover the range of numeric types. When no suitable match is found (e.g. no GraphQL type covers the range of
int64_t), it falls back toString.
psibase::PageInfo
struct psibase::PageInfo {
bool hasPreviousPage;
bool hasNextPage;
std::string startCursor;
std::string endCursor;
};
GraphQL support for paging.
This lets the query clients know when more data is available and what cursor values can be used to fetch that data.
psibase::Edge
template<typename Node, psio::FixedString EdgeName>
struct psibase::Edge {
Node node;
std::string cursor;
};
GraphQL support for paging.
node contains the row data. cursor identifies where in the
table the node is located.
psibase::Connection
template<typename Node, psio::FixedString ConnectionName, psio::FixedString EdgeName>
struct psibase::Connection {
std::vector<Edge> edges;
psibase::PageInfo pageInfo;
};
GraphQL support for paging.
edges contain the matching rows. pageInfo gives clients information
needed to resume paging.
psibase::EventDecoder
template<typename Events>
struct psibase::EventDecoder {
psibase::DbId db;
uint64_t eventId;
psibase::AccountNumber service;
};
GraphQL support for decoding an event.
If a GraphQL query function returns this type, then the system fetches and decodes an event.
The GraphQL result is an object with these fields, plus more:
type MyService_EventsUi {
event_db: Float! # Database ID (uint32_t)
event_id: String! # Event ID (uint64_t)
event_found: Boolean! # Was the event found in db?
event_service: String! # Service that created the event
event_supported_service: Boolean! # Is this service the one
# that created it?
event_type: String! # Event type
event_unpack_ok: Boolean! # Did it decode OK?
}
EventDecoder will only attempt to decode an event which meets all of the following:
- It's found in the
EventDecoder::dbdatabase (event_foundwill be true) - Was written by the service which matches the
EventDecoder::servicefield (event_supported_servicewill be true) - Has a type which matches one of the definitions in the
Eventstemplate argument
If decoding is successful, EventDecoder will set the GraphQL event_unpack_ok
field to true. It will include any event fields which were in the query request.
It will include all event fields if the query request includes the special field
event_all_content. EventDecoder silently ignores any requested fields which
don't match the fields of the decoded event.
EventDecoder example
This example assumes you're already serving GraphQL and have defined events for your service. It's rare to define a query method like this one; use EventQuery instead, which handles history, ui, and merkle events.
struct Query
{
psibase::AccountNumber service;
auto getUiEvent(uint64_t eventId) const
{
return EventDecoder<MyService::Events::Ui>{
DbId::uiEvent, eventId, service};
}
};
PSIO_REFLECT(Query, method(getUiEvent, eventId))
Example query:
{
getUiEvent(eventId: "13") {
event_id
event_type
event_all_content
}
}
Example reply:
{
"data": {
"getUiEvent": {
"event_id": "13",
"event_type": "credited",
"tokenId": 1,
"sender": "symbol-sys",
"receiver": "alice",
"amount": {
"value": "100000000000"
},
"memo": {
"contents": "memo"
}
}
}
}
psibase::EventQuery
template<typename Events>
struct psibase::EventQuery {
psibase::AccountNumber service;
history(...); // Decode history events
ui(...); // Decode user interface events
merkle(...); // Decode merkle events
};
GraphQL support for decoding multiple events.
If a GraphQL query function returns this type, then the system returns a GraphQL object with the following query methods:
type MyService_Events {
history(ids: [String!]!): [MyService_EventsHistory!]!
ui(ids: [String!]!): [MyService_EventsUi!]!
merkle(ids: [String!]!): [MyService_EventsMerkle!]!
}
These methods take an array of event IDs. They return arrays
of objects containing the decoded (if possible) events.
See EventDecoder for how to interact with the return values;
MyService_EventsHistory, MyService_EventsUi, and
MyService_EventsMerkle all behave the same.
EventQuery example
This example assumes you're already serving GraphQL and have defined events for your service.
struct Query
{
psibase::AccountNumber service;
auto events() const
{
return psibase::EventQuery<MyService::Events>{service};
}
};
PSIO_REFLECT(Query, method(events))
Example query:
{
events {
history(ids: ["3", "4"]) {
event_id
event_all_content
}
}
}
Example reply:
{
"data": {
"events": {
"history": [
{
"event_id": "3",
"tokenId": 1,
"creator": "token-sys",
"precision": {
"value": 8
},
"maxSupply": {
"value": "100000000000000000"
}
},
{
"event_id": "4",
"prevEvent": 1,
"tokenId": "3",
"setter": "token-sys",
"flag": "untradeable",
"enable": true
}
]
}
}
}
psibase::EventQuery::history
auto psibase::EventQuery::history(
const std::vector<uint64_t> & ids
) const;
Decode history events.
psibase::EventQuery::ui
auto psibase::EventQuery::ui(
const std::vector<uint64_t> & ids
) const;
Decode user interface events.
psibase::EventQuery::merkle
auto psibase::EventQuery::merkle(
const std::vector<uint64_t> & ids
) const;
Decode merkle events.
psibase::makeEventConnection
template<typename Events>
auto psibase::makeEventConnection(
psibase::DbId db,
uint64_t eventId,
psibase::AccountNumber service,
std::string_view fieldName,
std::optional<uint32_t> first,
const std::optional<std::string> & after
);