2019-04-04 11:33:38 +03:00
|
|
|
Navigator Backend
|
|
|
|
=================
|
|
|
|
|
|
|
|
The Navigator backend is a Scala application providing
|
|
|
|
|
2019-04-04 23:59:24 +03:00
|
|
|
- a web server that exposes a GraphQL API with some predefined endpoints,
|
2019-04-04 11:33:38 +03:00
|
|
|
such as returning all visible contracts.
|
|
|
|
- a platform client that reacts to events happening in the platform
|
|
|
|
- a simple sign-in API
|
|
|
|
|
|
|
|
Usage
|
|
|
|
-----
|
|
|
|
|
|
|
|
The Navigator backend is written in Scala, making heavy use of
|
|
|
|
[Akka](http://akka.io/) and [Sangria](http://sangria-graphql.org/) (for
|
|
|
|
GraphQL). It uses the [Scala Build Tool](http://www.scala-sbt.org/).
|
|
|
|
|
2019-04-04 23:59:24 +03:00
|
|
|
We can build and run a basic the backend using the following commands:
|
2019-04-04 11:33:38 +03:00
|
|
|
|
|
|
|
```bash
|
2019-04-04 23:59:24 +03:00
|
|
|
# Build a distribution archive ("fat jar"):
|
2019-05-17 12:22:05 +03:00
|
|
|
bazel build //navigator/backend:navigator-binary_deploy.jar
|
2019-04-04 23:59:24 +03:00
|
|
|
|
2019-04-04 11:33:38 +03:00
|
|
|
# Run without arguments to show usage:
|
2019-06-26 17:02:59 +03:00
|
|
|
java -jar bazel-bin/navigator/backend/navigator-binary_deploy.jar --help
|
2019-04-04 11:33:38 +03:00
|
|
|
|
|
|
|
# Create a dummy configuration file
|
|
|
|
cat << EOF > navigator.conf
|
|
|
|
users {
|
|
|
|
OPERATOR: {
|
|
|
|
party=OPERATOR
|
|
|
|
password=operator_password
|
|
|
|
}
|
|
|
|
}
|
|
|
|
EOF
|
|
|
|
|
2019-04-04 23:59:24 +03:00
|
|
|
# Start the web server
|
2019-06-26 17:02:59 +03:00
|
|
|
java -jar bazel-bin/navigator/backend/navigator-binary_deploy.jar server -c navigator.conf
|
2019-04-04 11:33:38 +03:00
|
|
|
```
|
|
|
|
|
|
|
|
If you start the server and the configuration file doesn't exist, the server will
|
|
|
|
write a configuration template and quit. You can then edit the configuration file
|
|
|
|
and start again the server.
|
|
|
|
|
|
|
|
Custom UI backends
|
|
|
|
------------------
|
|
|
|
|
|
|
|
## Extend the `UIBackend` base class
|
|
|
|
|
|
|
|
The base class for UI backends is the
|
|
|
|
[`UIBackend`](src/main/scala/com/digitalasset/navigator/backend/UIBackend.scala),
|
|
|
|
a Scala class that implements a ledger indexer and a web-server which exposes a
|
|
|
|
number of default GraphQL endpoints and a simple Session API. It also allows has
|
|
|
|
hooks for adding custom GraphQL endpoints.
|
|
|
|
|
|
|
|
To create a basic UI backend, you can extend `UIBackend` without any custom
|
|
|
|
additions:
|
|
|
|
|
|
|
|
```scala
|
|
|
|
object DefaultUIBackend extends UIBackend {
|
|
|
|
override def customEndpoints: Set[CustomEndpoint[_]] = Set()
|
|
|
|
override val defaultConfigFile: Path = Paths.get("my-app.conf")
|
2019-04-04 23:59:24 +03:00
|
|
|
override def applicationInfo: ApplicationInfo = ApplicationInfo(...)
|
|
|
|
override def banner: Option[String] = Some(...)
|
2019-04-04 11:33:38 +03:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
You can then run your app with this as the main class.
|
|
|
|
|
|
|
|
## Custom endpoints
|
|
|
|
|
|
|
|
A custom endpoint is an extra endpoint for a UI backend that binds a name to
|
|
|
|
some data that can be represented in GraphQL and a function to calculate that
|
|
|
|
data. For example:
|
|
|
|
|
|
|
|
```scala
|
|
|
|
object ExampleUIBackend extends UIBackend {
|
|
|
|
|
|
|
|
override def customEndpoints: Set[CustomEndpoint[_]] = Set(contractsIdEndpoint)
|
|
|
|
|
|
|
|
final case class TemplateId(id: String)
|
|
|
|
final case class ContractProjection(id: String, template: TemplateId, argument: RecordArgument)
|
|
|
|
|
|
|
|
/** Endpoing for the contract ids */
|
|
|
|
private val contractsIdEndpoint = new CustomEndpoint[ContractProjection] {
|
|
|
|
/** The endpoint to be used as GraphQL top-level for the data served by this */
|
|
|
|
override def endpointName: String = "contract_id"
|
|
|
|
|
|
|
|
/** For each contract in the ledger, extract contract id and template id */
|
|
|
|
override def calculate(ledger: Ledger): Seq[ContractProjection] =
|
|
|
|
ledger.allContracts().map(contract =>
|
|
|
|
ContractProjection(contract.idString, TemplateId(contract.template.idString), contract.argument))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
This adds an endpoint called `contract_id` that returns a list of contract
|
|
|
|
projections that include the contract ID, the template ID, and the contract
|
|
|
|
arguments. You would then pass this custom endpoint to a UI backend subclass:
|
|
|
|
|
|
|
|
There are two components of a `CustomEndpoint`:
|
|
|
|
|
|
|
|
- The `endpointName` is the name used to bind this custom endpoint to the root
|
|
|
|
of the GraphQL Schema. Note that each endpoint must have a unique
|
|
|
|
`endpointName` and that default `endpointName`s -- `contracts` or `templates`
|
|
|
|
for example -- cannot be used in custom endpoints either.
|
|
|
|
|
|
|
|
- The `calculate` function is used to extract the data to serve from the
|
|
|
|
[`Ledger`](src/main/scala/com/digitalasset/ui/backend/model/Ledger.scala).
|
|
|
|
This function must create a sequence of instances of a scala *case class*.
|
|
|
|
|
|
|
|
The endpoint `contract_id` becomes available in the root of the graphql schema:
|
|
|
|
|
|
|
|
```graphql
|
|
|
|
type ContractProjection {
|
|
|
|
id: String!
|
|
|
|
template: TemplateId!
|
|
|
|
argument: Value!
|
|
|
|
}
|
|
|
|
|
|
|
|
type TemplateId {
|
|
|
|
id: String!
|
|
|
|
}
|
|
|
|
|
|
|
|
type Query {
|
|
|
|
contract_id: [ContractProjection!]!
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
The schema is automatically generated by the UI backend by following the
|
|
|
|
structure of a "row" case class. For instance `TemplateId` is a nested structure
|
|
|
|
inside `ContractProjection` in the Scala code and for this reason it will be a
|
|
|
|
nested structure in the GraphQL Schema too.
|
|
|
|
|
|
|
|
## Automatic generation of GraphQL Schemas for custom endpoints
|
|
|
|
|
|
|
|
The code for the GraphQL endpoint is automatically generated at compile time by
|
|
|
|
the UI backend. This means that there are some limitations in what can be
|
|
|
|
exposed as result of the `calculate` function. Specifically:
|
|
|
|
|
|
|
|
- The name of the "row" model case class must be a unique name. Don't use
|
|
|
|
`Contract` or `Template` or any other name already used in the [`GraphQL
|
|
|
|
Schema`](src/main/scala/com/digitalasset/ui/backend/graphql/GraphQLSchema.scala).
|
|
|
|
For example, you can't rename `ContractProjection` to `Contract`.
|
|
|
|
|
|
|
|
- The main "row" class must be modelled as a Scala case class and it must
|
|
|
|
contain fields that are either a case class or Scala primitives. If something
|
|
|
|
is not supported, you won't be able to compile your code.
|
|
|
|
|
|
|
|
If some type is not supported by the generator, it is possible to add support
|
|
|
|
for it by providing either an instance of the typeclass
|
|
|
|
[`GraphQLLeaf`](src/main/scala/com/digitalasset/ui/backend/graphqless/GraphQLLeaf.scala)
|
|
|
|
for simple types, e.g. `String` or `Int`, or an instance of the typeclass
|
|
|
|
[`GraphQLObject`](src/main/scala/com/digitalasset/ui/backend/graphqless/GraphQLObject.scala)
|
|
|
|
for complex types which have fields.
|
|
|
|
|
|
|
|
Common tasks
|
|
|
|
------------
|
|
|
|
|
|
|
|
## How do I query the Backend without a frontend?
|
|
|
|
|
|
|
|
The UI backend exposes a [GraphiQL](https://github.com/graphql/graphiql) page to
|
|
|
|
let the developer run queries interactively. You can find it at the address
|
|
|
|
`<address_of_the_navigator_backend>/graphql`. Note the "Docs" link in the upper
|
|
|
|
right corner and also that you have to be logged in order to run queries. You
|
|
|
|
can either use the frontend in the same browser to login or you can login
|
|
|
|
manually via command line as follows:
|
|
|
|
|
|
|
|
- Send a JSON POST request with the `userId` and the `password` and make note of
|
|
|
|
the cookie:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
> curl -H "Content-Type: application/json" -d'{ "userId":"PARTY", "password":"password" }' localhost:4000/api/session/ -i
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Set-Cookie: session-id=8b4601d4-7113-407b-9b81-7fd5b213a96b; Path=/
|
|
|
|
Server: akka-http/10.0.4
|
|
|
|
Date: Tue, 13 Jun 2017 16:47:38 GMT
|
|
|
|
Content-Type: application/json
|
|
|
|
Content-Length: 92
|
|
|
|
|
|
|
|
{"type":"session","user":{"id":"BANK1","role":"bank","party":"BANK1","canAdvanceTime":true}}
|
|
|
|
```
|
|
|
|
|
|
|
|
- Open the GraphiQL page in a browser (`localhost:4000/graphql` for example).
|
|
|
|
|
|
|
|
- Write `javascript:document.cookie="session-id=<the_id_recceived>"` in the location bar.
|
|
|
|
|
|
|
|
- You can now use the GraphiQL to query your data.
|
|
|
|
|
|
|
|
We plan to add an option to run the server in unauthenticated mode so that these steps become unnecessary.
|
|
|
|
|
|
|
|
Session API
|
|
|
|
-----------
|
|
|
|
|
|
|
|
In addition to the GraphQL endpoint exposing data, the user needs to act as
|
|
|
|
some party. This is to know which party's "view" on the ledger to expose. The
|
|
|
|
backend supplies the frontend with a list of available users. Alternatively, if
|
|
|
|
the backend is started with the `--require-password` flag, it will not supply a
|
|
|
|
list of users and instead ask the frontend to present the user with a standard
|
|
|
|
username/password form. The chosen user is set in a cookie and therefore
|
|
|
|
persists across reloads.
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
type UserId = string;
|
|
|
|
type Party = string;
|
|
|
|
type Role = string;
|
|
|
|
type User = {
|
|
|
|
id: UserId;
|
|
|
|
party: Party;
|
|
|
|
canAdvanceTime: boolean;
|
|
|
|
role?: Role;
|
|
|
|
};
|
|
|
|
|
|
|
|
type Status = Session | SignIn;
|
|
|
|
type Session = { type: 'session'; user: User; }
|
|
|
|
type SignIn = {
|
|
|
|
type: 'sign-in';
|
|
|
|
method: SignInMethod;
|
|
|
|
error?: 'invalid-credentials';
|
|
|
|
}
|
|
|
|
|
|
|
|
type SignInMethod = SignInPassword | SignInSelect
|
|
|
|
type SignInPassword = { type: 'password' }
|
|
|
|
type SignInSelect = { type: 'select', users: UserId[] }
|
|
|
|
```
|
|
|
|
|
|
|
|
```bash
|
|
|
|
# Get current session or sign in
|
|
|
|
GET /session/ => Status
|
|
|
|
|
|
|
|
# Sign in
|
|
|
|
POST /session/ -d'{ userId: UserId, password?: String }' => Status
|
|
|
|
|
|
|
|
# Sign out
|
|
|
|
DELETE /session/ => SignIn
|
|
|
|
```
|
|
|
|
|
|
|
|
Architecture notes
|
|
|
|
------------------
|
|
|
|
|
|
|
|
This section contains short notes for anyone that wishes to make changes to the UI backend library.
|
|
|
|
|
|
|
|
- The backend defines its own internal representation of ledger objects
|
|
|
|
- The backend communicates with the ledger via a JSON-based "ledger API"
|
|
|
|
- The backend communicates with the frontend via a JSON-based "frontend API"
|
|
|
|
- These are the relevant source files:
|
|
|
|
- `model/Model`: Defines the internal representations of ledger objects
|
|
|
|
- `model/Util`: (SDAML package) -> (internal model)
|
|
|
|
- `graphql/JsonType`: (internal model) <-> (frontend API JSON format)
|
2019-06-26 17:02:59 +03:00
|
|
|
- `store/platform/PlatformSubscriber`: (ledger API JSON format) <-> (internal model)
|