C++ Web Services
Routing
psinode
passes most HTTP requests to the SystemService::HttpServer 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::CommonApi provides services common to all domains under the /common
tree. It also serves the chain's main page.
SystemService::Sites provides web hosting for non-service accounts or service accounts that did not register for HTTP handling.
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::HttpServer service's SystemService::HttpServer::registerServer action. There are multiple ways to do this:
psibase deploy
has a--register-proxy
option (shortcut-p
) that can do this while deploying the service.psibase register-proxy
can also do it. TODO: implementpsibase register-proxy
.- A service may call
registerServer
during its own initialization action.
A service doesn't have to serve HTTP requests itself; it may delegate this to another service during registration.
HTTP 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.
http-server
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(
HttpRequest request
);
Handle HTTP requests.
Define this action in your service to handle HTTP requests. You'll also need to register your service with http-server.
serveSys
can do any of the following:
- Return
std::nullopt
to 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::HttpServer 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.
If you implement this interface, you must also implement the psibase::ServerInterface in order to serve the files over HTTP.
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.html
for 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
- psibase::historyQuery
- psibase::QueryableService
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 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 ifIncludeRoot
is 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 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 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 fracpack, 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 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 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 /graphql
withContent-Type = application/graphql
: Run query that's in body and return JSON result.POST /graphql
withContent-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:
rowsByPrimary
androwsBySecondary
automatically havemakeConnection
's arguments.MyTypeEdge
andMyTypeConnection
are automatically generated fromMyType
.- Returned rows (
MyType
) include MyType's fields and thesomeFn
method. Onlyconst
methods 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;
psio_get_reflect_impl(...);
};
GraphQL support for paging.
node
contains the row data. cursor
identifies where in the
table the node is located.
psibase::Edge::psio_get_reflect_impl
psio_reflect_impl_Edge psibase::Edge::psio_get_reflect_impl(
const Edge<Node, EdgeName> &
);
psibase::Connection
template<typename Node, psio::FixedString ConnectionName, psio::FixedString EdgeName>
struct psibase::Connection {
std::vector<Edge> edges;
PageInfo pageInfo;
psio_get_reflect_impl(...);
};
GraphQL support for paging.
edges
contain the matching rows. pageInfo
gives clients information
needed to resume paging.
psibase::Connection::psio_get_reflect_impl
psio_reflect_impl_Connection psibase::Connection::psio_get_reflect_impl(
const Connection<Node, ConnectionName, EdgeName> &
);
psibase::EventDecoder
template<typename Events>
struct psibase::EventDecoder {
DbId db;
uint64_t eventId;
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::db
database (event_found
will be true) - Was written by the service which matches the
EventDecoder::service
field (event_supported_service
will be true) - Has a type which matches one of the definitions in the
Events
template 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",
"receiver": "alice",
"amount": {
"value": "100000000000"
},
"memo": {
"contents": "memo"
}
}
}
}
psibase::EventQuery
template<EventType Events>
struct psibase::EventQuery {
AccountNumber service;
history(...); // Decode history events
ui(...); // Decode user interface events
merkle(...); // Decode merkle events
psio_get_reflect_impl(...);
};
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": "tokens",
"precision": {
"value": 8
},
"maxSupply": {
"value": "100000000000000000"
}
},
{
"event_id": "4",
"prevEvent": 1,
"tokenId": "3",
"setter": "tokens",
"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::EventQuery::psio_get_reflect_impl
psio_reflect_impl_EventQuery psibase::EventQuery::psio_get_reflect_impl(
const EventQuery<Events> &
);
psibase::makeEventConnection
template<typename Events>
auto psibase::makeEventConnection(
DbId db,
uint64_t eventId,
AccountNumber service,
std::string_view fieldName,
std::optional<uint32_t> first,
const std::optional<std::string> & after
);
psibase::historyQuery
template<typename Tables, typename Events, typename RecordType, typename PrimaryKeyType>
auto psibase::historyQuery(
AccountNumber service,
uint64_t RecordType::* eventHead,
std::string_view previousEventFieldName,
PrimaryKeyType key,
auto first,
auto after
);
Helper function to allow graphql queries of History event chains.
psibase::QueryableService
template<typename Tables, typename Events = void>
struct psibase::QueryableService {
AccountNumber service;
index(...);
allEvents(...);
eventIndex(...);
};
Construct a QueryableService object to simplify the implementation of a GraphQL QueryRoot object for your service..
The class template takes the Tables and Events objects as template parameters, and is constructed with the account number at which your service is deployed. E.g.
auto myService = QueryableService<MyService::Tables, MyService::Events>{"myservice"};
A QueryableService object can then be used to simplify querying table indices and historical events in the QueryRoot object definition. E.g.
struct MyQueryRoot
{
auto readTableA() const
{ //
return myService.index<TableA, 0>();
}
auto events() const
{ //
return myService.allEvents();
}
auto userEvents(AccountNumber user,
optional<uint32_t> first,
const optional<string>&
after) const
{
return myService.eventIndex<MyService::UserEvents>(user, first, after);
}
};
PSIO_REFLECT(MyQueryRoot,
method(readTableA),
method(events),
method(userEvents, user, first, after))
psibase::QueryableService::index
template<typename TableType, int Idx>
auto psibase::QueryableService::index();
psibase::QueryableService::allEvents
auto psibase::QueryableService::allEvents();
psibase::QueryableService::eventIndex
template<typename EventType>
auto psibase::QueryableService::eventIndex(
auto key,
std::optional<uint32_t> first,
const std::optional<std::string> & after
);