C++ Web Services

Routing


  
  
    
      
    
    
      
    
    
      
    
    
      
    
    
      
    
  
  
  
  http
  
  server
  service
  
  sites
  service's
  serveSys
  action
  
  registered
  service's
  serveSys
  action
  
  psinode
  
  HTTP
  Request
  
  Common
  service's
  serveSys
  action
  no
  yes
  no
  yes
  no
  yes
  target
  with
  begins
  on
  a
  subdomain?
  registered?
  /common?
  
    
    
  
  
    
    
  
  
    
    
    
    
    
    
  
  
    
    
  
  
    
    
    
    
    
    
  
  
    
    
    
    
    
  
  
    
    
  
  
    
    
    
    
    
    
  
  
    
    
    
    
  
  
    
    
  
  
    
    
    
    
    
  
  
    
    
  

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: implement psibase 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

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 page
  • contentType: 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:

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 serveActionTemplates
  • POST /pack_action/x: provided by servePackAction
  • GET /, but only if IncludeRoot 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 with Content-Type = application/graphql: Run query that's in body and return JSON result.
  • POST /graphql with Content-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.

See makeConnection example.

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: Connection
  • T: Type stored in index
  • Key: Key in index

Arguments:

  • index: TableIndex to paginate through
  • gt: Restrict range to keys greater than this
  • ge: Restrict range to keys greater than or equal to this
  • lt: Restrict range to keys less than to this
  • le: Restrict range to keys less than or equal to this
  • first: Stop after including this many items at the beginning of the range
  • last: Stop after including this many items at the end of the range
  • before: Opaque cursor value. Resume paging; include keys before this point
  • after: 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 and rowsBySecondary automatically have makeConnection's arguments.
  • MyTypeEdge and MyTypeConnection are automatically generated from MyType.
  • Returned rows (MyType) include MyType's fields and the someFn method. Only const 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 to String.

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
);