Skip to content

Latest commit

 

History

History
1839 lines (1449 loc) · 68.2 KB

File metadata and controls

1839 lines (1449 loc) · 68.2 KB

Server API Documentation

This document describes the REST Application Programming Interfaces (APIs) supported by an Ego server instance. This covers how to authenticate to the server, administration functions that can be performed by a suitably privileged user, APIs for directly accessing database tables and their data, and APIs for accessing user-written services (implemented as Ego programs)

Table of Contents

  1. Introduction

  2. Authentication

  3. Administration

  4. Tables

  5. Services

   

Introduction

This section covers basic information about using the API functions built into the Ego server.

All REST API responses should return a server information object as a field in the payload named "server". This contains the following information:

Field Description
api An integer indicating the API version. Currently always 1.
name The short host name of the machine running the server.
id The UUID of this instance of the server.
session The session correlator for the server (matches request log entries).

The server name can be used to locate where the log files are found. The server id helps identify when a server is restarted, as it is assigned a new UUID each time and this information is in the server log. Finally, each logging entry from a REST client session includes a session number. This integer value can be used to find the specific log entries for this request.

   

Authentication

Other than determining if a server is running or not, all operations performed against an Ego server must be done with an authenticated user. The normal pattern for this is for the client to "log in" the user, and receive an encrypted token in return. This token can be presented to any other API to authenticate the user for the functions of that API.

To authenticate, you can use a "GET" or a "POST method to the endpoint "/services/admin/logon".

GET /services/admin/logon

Use GET when you wish to use Basic authentication in the HTTP header to send the username and password for logon. The request must:

  • Use BASIC authentication to pass the username and password to the service.
  • Specify that the reply type accepted is "application/json"

The rest status will either be a status of 403 indicating that the credentials are invalid, or 200 indicating that the credentials were valid.

The response payload is a JSON object with the resulting secure token if the credentials were valid. The credentials must match the username and password stored in the Ego server credentials data store; you can use ego CLI commands to view and modify this store, as well as /admin endpoints described below to read, update, or delete entries in the credentials store.

The resulting JSON payload is an object with the following fields:

Field Description
server The server information object for this response.
expires A string containing the timestamp of when the token expires
token A variable-length string containing the token text itself.
identity The username encoded within the token.

Here is an example response payload:  

{
    "server": {
        "api": 1,
        "name": "appserver.abc.com",
        "id": "2ef21c8f-cc4f-4a83-9e62-b7b7561c64ce",
        "session": 151
    },
    "expires": "Fri Jan 21 13:12:25 EST 2022",
    "identity": "joesmith",
    "token": "220de9776c7c517f84c1d4b94aadcb6e50849abed4eb6b26b9d16e3365e3a014b5fdefac5b107"
}

 

It is the responsibility of the application to extract the token field from the resulting payload and store it away to use for subsequent REST API operations. When using this token, it should be used as a Bearer token in subsequent REST operations as the Authentication: Bearer header.

A less secure mechanism can be used to authenticate, by providing username and password credentials using the Authentication: Basic header, followed by a Base64 encoding of the "username:password" string. This can be used for initial development and debugging, but an authenticated token is the preferred way to interact with the table services. In the future, the Basic authentication support may be removed, requiring a Bearer token authentication.

   

POST /services/admin/logon

Alternatively, if you do not want to use an authentication header in this initial communication, you can use a "POST" method to the same endpoint with a JSON payload with an object containing two field. For TLS/SSL-based communication, this embeds the credentials inside the encrypted body of the request which can be more secure. The request body must contain the following two fields:

 

Field Description
username A string containing username of the credentials
password A string containing password of the credentials

Here is an example request payload for the logon operation, with a string for the username and a string for the password:

{
    "username": "joesmith",
    "password": "3h97q-k35Z5"
}

 

The REST call will result in either be a status of 403 indicating that the credentials are invalid, or 200 indicating that the credentials were valid.

The response payload is a JSON object with the resulting secure token if the credentials were valid. The credentials must match the username and password stored in the Ego server credentials data store; you can use ego CLI commands to view and modify this store, as well as /admin endpoints described below to read, update, or delete entries in the credentials store.

The resulting JSON payload is an object with the following fields:

Field Description
server The server information object for this response.
expires A string containing the timestamp of when the token expires.
token A variable-length string containing the token text itself.
identity The username encoded within the token.

Here is an example response payload:  

{
    "server": {
        "api": 1,
        "name": payroll,
        "id": "2ef21c8f-cc4f-4a83-9e62-b7b7561c64ce",
        "session": 482
    },
    "expires": "Fri Jan 21 13:12:25 EST 2022",
    "identity": "joesmith",
    "token": "220de9776c7c517f84c1d4b94aadcb6e50849abed4eb6b26b9d16e3365e3a014b5fdefac5b107"
}

 

It is the responsibility of the application to extract the token field from the resulting payload and store it away to use for subsequent REST API operations. When using this token, it should be used as a Bearer token in subsequent REST operations as the Authentication: Bearer header.

A less secure mechanism can be used to authenticate, by providing username and password credentials using the Authentication: Basic header, followed by a Base64 encoding of the "username:password" string. This can be used for initial development and debugging, but an authenticated token is the preferred way to interact with the table services. In the future, the Basic authentication support may be removed, requiring a Bearer token authentication.

   

Administrative Functions

Administrative functions are REST APIS used to support managing the REST server, including the status and state of the server, the database of valid user credentials and permissions, and support for caching and logging functions on the server.

   

Heartbeat

The heartbeat endpoint is the simplest and fasted way to determine if an Ego server is running and responding to requests. It does not require authentication of any kind, and returns a 200 success code if the server is available. Any other return code indicates that the server is not running or there is a network/gateway problem between the REST client code and the Ego server.

This endpoint only supports the GET method, and returns no response body.    

Caches

The Ego server maintains caches to make repeated use of the server more efficient by saving operations in memory instead of having to re-load and re-compile services, reload assets, etc.

You can examine what is in the server cache, direct the server to flush (i.e. evict from memory) any cached items, and you can set the size of the services cache using REST calls to the admin/caches endpoint.

When a service is invoked by a user to execute Ego code written by the developer(s) running the Ego server, the server first searches the cache to see if it has already executed this code before. When this is the case, the server does not have to reload the service source code from disk or compile it again. Instead, it uses the previous results of the compile to execute the service again on behalf of the client.

When the service cannot be found in the cache, it is loaded from disk and compiled before it can be executed. The service just compiled is placed in the cache. If the cache is too large (based on the limit on the number of items the server is configured to allow) the oldest (least recently used) item in the cache is discarded before storing the newly-compiled service in the cache. For example, if the cache limit is set to 10, then the cache will contain the ten most-recently used services. The premise is that the cache should be set large enough to hold the most commonly used services, so they are available for most users most of the time without recompiling.

Similarly, an "asset" cache is managed by the server. When a REST call is made to the server to the /asset endpoint, the remainder of the path represents the location in the Ego server's disk storage where assets are found. When a request is made for an item, the server first checks to see if it is in memory already, and if so will just return the contents of the cached item. If it was not found, then the item is loaded from disk and also stored in the cache. The assets are typically image files or similar HTML assets that might be used by a browser-based application. As such, the asset cache is limited by the number of bytes of storage that it can consume in memory, regardless of the number of assets. By default, the cache is one megabyte in size.

Note that a side effect of having a non-zero asset cache size is that if a server has already cached a value in memory, then changing the disk copy will not result in the updated information being used as the cached value. For example, if an HTML web page requests an image from the /assets path, and then after that the developer changes the image on disk using an image editing program, subsequent calls to the server will still return the old copy. You must flush the server cache to cause it to discard all the asset storage and start again reading from disk to satisfy asset requests.

   

GET /admin/caches

This gets information about the caching status in the server. This API requires that the user have "admin" privileges. The result is a JSON payload with the following fields:  

Field Description
server The server information object for this response
host A string containing the name of the computer running the Ego server
id A string containing the UUID of the server instance
serviceCount The number of items in the services cache
serviceSize The maximum number of items in the services cache
items An array of objects for each cached item
assetCount The number of assets stored in the in-memory cached
assetSize The maximum size in bytes of the asset cache

 

The items array contains an object for each item in the cache. The contents of this object are defined as:

Field Description
name The object name (the endpoint path used to locate it)
last A string representation of the last time the item was used
class Either "asset" or "service"
count The number of times the item has been accessed

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

DELETE /admin/caches

The DELETE REST method tells the Ego server to flush the caches; i.e. all the copies of service compilations and asset objects are deleted from memory. Subsequent REST calls will require that the server reload the item(s) from the disk store and also then store them in the cache for future use.

You must have "admin" privileges to execute this REST call.    

PUT /admin/caches

You can set the size of the caches using the PUT method. The JSON payload for this operation is a structure with one or both of the following fields:

Field Description
limit The maximum number of items in the services cache
assetSize The maximum size in bytes of the asset cache

   

You must be an "admin" user to execute this call.  

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

Loggers

You can use the loggers endpoint to get information about the current state of logging on the server, enable or disable specific loggers, and retrieve the text of the log.    

GET /admin/loggers

This retrieves the current state of logging on the server. The response is a JSON payload that indicates the host name where the server is running, it's unique instance UUID, the name of the text file on the server where the log is being written, and an structure that indicates if each logger is enabled or disabled.

This service requires authentication with credentials for a user with administrative privileges.

Here is an example response payload from this request:  

{
    "server": {
        "api": 1,
        "name": "appserver.abc.com",
        "id": "2ef21c8f-cc4f-4a83-9e62-b7b7561c64ce",
        "session": 6385
    },
    "file": "/Users/tom/ego/ego-server_2022-01-20-000000.log",
    "loggers": {
        "APP": false,
        "AUTH": true,
        "BYTECODE": false,
        "CLI": false,
        "COMPILER": false,
        "DB": false,
        "DEBUG": false,
        "INFO": false,
        "REST": true,
        "SERVER": true,
        "SYMBOLS": false,
        "TABLES": true,
        "TRACE": false,
        "USER": false
    }
}

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

GET /services/admin/log

This path will return the text of the log file itself. If you specify that the REST call accepts application/json, it will be returned as an array of strings, each containing a JSON payload of a single log line, in non-localized form. If you specify that the call accepts application/text, the log file is localized on the server side (using the server's locale) and is then returned as-is as lines of raw text.

If you add the parameter ?tail=n where n is a number of lines of text, the GET operation will return the last lines from the log. If you specify a value of zero, then all lines are returned, otherwise the result is limited to the last n lines of the log

Here is an example output with a tail value of 5:  

{
    "server": {
        "api": 1,
        "name": "appserver.abc.com",
        "id": "2ef21c8f-cc4f-4a83-9e62-b7b7561c64ce",
        "session": 91103
    },
    "lines": [
    "{\"time\":\"2025-12-29 10:57:55\",\"id\":\"e0ca934f-c05e-45e6-b10c-90a2b2c41043\",\"seq\":482,\"session\":81,\"class\":\"server\",\"msg\":\"log.server.request\",\"args\"{\"elapsed\":\"1.282333ms\",\"host\":\"[::1]:50239\",\"length\":\"0\",\"method\":\"DELETE\",\"path\":\"/admin/tokens/2a41839f-4c4c-40a6-afde-35427331c21e\",\"status\":200,\"type\":\"text\",\"user\":\"admin\"}}",
    "{\"time\":\"2025-12-29 10:57:55\",\"id\":\"e0ca934f-c05e-45e6-b10c-90a2b2c41043\",\"seq\":483,\"session\":82,\"class\":\"server\",\"msg\":\"log.server.request\",\"args\":{\"elapsed\":\"3.666667ms\",\"host\":\"[::1]:50240\",\"length\":\"10\",\"method\":\"GET\",\"path\":\"/services/factor/10\",\"status\":200,\"type\":\"application/json\",\"user\":\"\"}}",
    "{\"time\":\"2025-12-29 10:57:55\",\"id\":\"e0ca934f-c05e-45e6-b10c-90a2b2c41043\",\"seq\":484,\"session\":83,\"class\":\"server\",\"msg\":\"log.server.request\",\"args\":{\"elapsed\":\"338.375µs\",\"host\":\"[::1]:50241\",\"length\":\"28\",\"method\":\"GET\",\"path\":\"/services/factor/10s\",\"status\":400,\"type\":\"application/json\",\"user\":\"\"}}"
    "{\"time\":\"2025-12-29 10:57:55\",\"id\":\"e0ca934f-c05e-45e6-b10c-90a2b2c41043\",\"seq\":485,\"session\":84,\"class\":\"server\",\"msg\":\"log.server.request\",\"args\":{\"elapsed\":\"461.583µs\",\"host\":\"[::1]:50242\",\"length\":\"14\",\"method\":\"GET\",\"path\":\"/services/factor/10\",\"status\":200,\"type\":\"text\",\"user\":\"\"}}
    {\"time\":\"2025-12-29 10:57:55\",\"id\":\"e0ca934f-c05e-45e6-b10c-90a2b2c41043\",\"seq\":486,\"session\":85,\"class\":\"server\",\"msg\":\"log.server.request\",\"args\":{\"elapsed\":\"1.269292ms\",\"host\":\"[::1]:50243\",\"length\":\"3\",\"method\":\"GET\",\"path\":\"/services/count\",\"status\":200,\"type\":\"application/json\",\"user\":\"\"}}"
    ]
}

Note that the "lines" array contains escaped strings, each containing a single JSON object for each line of the log returned. The format of each line is the same, with the exception that the "args" object contains varying fields representing additional data for the specific log entry, which is always defined by the "msg" object.

Here is the complete list of defined fields in a log message object:

Field Name Description
"time" The timestamp of when the log entry was generated
"id" The UUID of the server that generated the message
"seq" An integer sequence number that can order messages for a given id
"session" The REST session number that generated this log message
"class" The Ego logging message class ("server", "rest", "auth", etc.)
"msg" A text string uniquely identifying each message
"args" An object with named arguments that accompany the message

   

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

PUT /admin/loggers

This call is used to modify the state of logging on the server. The payload must contain the loggers structure that tells which loggers are to change state. Note that any logger not mentioned in the payload does not have it's state changed.

This service requires authentication with credentials for a user with administrative privileges. The response is the same as the GET operation; a summary of the current state of logging.

Here is a sample request body, that enables the INFO logger and disables the TRACE logger. Note that the names of the loggers are not case-sensitive.  

{
"loggers": {
    "INFO": true,
    "TRACE": false
    }
}

   

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

Users

The users interface allows an administrative user to create and delete user credentials, set user passwords, and update the permissions list for a given user.

   

GET /admin/users/

This call returns the list of users that are in the credentials store. The result is a JSON structure with the following fields:  

Field Description
server The server information object for this response
start This value is always zero
count The number of items returned
items An array of user objects, described in the next table

 

The field "items" contains an array of user objects. The array of user objects has the following fields:  

Field Description
name The name of the user
id A unique UUID for the user
permissions an array of strings containing permissions names

 

Here is example output from a request to this endpoint:  

{
    "server": {
        "api": 1,
        "name": "appserver.abc.com",
        "id": "2ef21c8f-cc4f-4a83-9e62-b7b7561c64ce",
        "session": 155
    },
    "start": 0,
    "count": 2,
    "items": [
        {
            "name": "admin",
            "id": "0b77ac93-44b3-4f43-b1d3-9fa0dc7a4039",
            "permissions": [
                "ego.root",
                "ego.logon"
            ]
        },
        {
            "name": "iphoneUser",
            "id": "360565a1-f038-4478-88f3-abd9cc38d47f",
            "permissions": [
                "ego.logon",
                "ego.table.admin"
            ]
        }
    ]
}

 

In this example, there are two users defined. The user "admin" has the root privilege in their list, which makes them an administrative user. The user "iphoneUser" has logon and table_create privileges, which enable this user to connect to the server and have permission to create tables using the /tables API discussed below.

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

Assets

The Ego server has the ability to serve up arbitrary file contents to a REST caller. These are referred to as "assets" and are typically things like image files, javascript payloads, etc. that are created by the administrator of an instance of an Ego web server, to support services written in Ego.

The only supported method is "GET"

GET /assets/path

The "GET" operation reads an asset from the disk in the library part of the Ego path. The root of this location is typically EGO_PATH/lib/services/assets/ followed by the path expressed in the REST URL call. You can only GET items, you cannot modify them or list them.

Note that when an asset is read, it is also cached in memory (see the documentation on caching for more information). You can also see an example of an asset being read in the service located at EGO_PATH/lib/services/templates/memory.html which references an asset in HTML for a graphical image.

     <!-- The asset must have a root path of /assets to be located properly --> 
     <img src="/assets/logo.png" alt="Ego logo" style="width:300px;height:150px;">

   

Tables

The Ego server includes a REST API for communicating with a database configured for use by the server. This can be either the default database (using the /tables endpoint) or a data source name (DSN) using the /dsns API. These APIs are used to manage database tables and read, write, update, and delete rows from tables.

When accessing data via the DSN endpoints, the URL path is /dsns/ followed by the table name, followed by /tables. For example, to read the rows of a table named foo in a DSN named bar, then GET endpoint URL is

/dsns/bar/tables/foo/rows

Similarly, to access the same table in the default database (if the default database is configured on the server) the endpoint would be

/tables/foo/rows

which does not specify a DSN. This same pattern applies to all the table access endpoints described below. See the separate section on defining and setting permissions for data source names.

Each API is divided into two sets of endpoint functions,

   

Table API

This section covers APIs to:

All tables operations return either a rowset or a rowcount response. A rowset contains an array of structure definitions where each column in the row is the field name, and the value of the column in that row is the field value. There will be one object for each column in the requested table query. A rowcount contains a struct with a field called "count" which is the number of rows that are affected by the operation performed. For update or delete operations, this is the number of rows that were updated or deleted. This value is zero for other operations (like deleting a table).

Finally, rowsets and rowcounts will also include a status field which is the HTTP status of the operation, which is normally 200 for a successful operation. A value other than 200 means something happened with the request that may not be the desired result, so an additional field msg contains the text of any error message generated (for example, attempting to read a table column that doesn't exist, or not having permissions for the requested operation).

It is recommended that you read the API on the "rows" endpoints before attempting to use the transaction function, as each task in the transaction is generally the same as an individual row operation, and there is detailed documentation in the rows API for things like filter specifications, etc.

   

GET /tables

A GET call to the /tables endpoint will return a list of the tables. This is a JSON payload containing an array of objects, each of which describes a table that the current user has read access to.

Because the list of tables might be quite long, you can specify URL parameters that limit the result set:

Parameter Example Description
limit ?limit=10 Return at most this many rows from the result set
start ?start=100 Specify the first row of the result set (1-based)
rowcounts ?rowcounts=false Do not return row counts in the result

   

The rowcounts parameter defaults to true; the tables list operation will include the number of rows in the table in the result set. However, for very large tables this may become a performance problem, so the caller can request that the server not get the row count. When rowcounts is set to false, then the row count is always zero.

The result of the call is an object with two fields, count and tables. The count is the number of tables returned in this REST call. The tables are an array of table objects, with the following fields:

Parameter Type Description
name string The name of the table
schema string The username for the database schema
columns int Count of columns in the table
rows int Count of rows in the table

   

Here is an example of the result data when the call is made by the user "smith", returning three available tables of info:

 

{
    "server": {
        "api": 1,
        "name": "appserver.abc.com",
        "id": "2ef21c8f-cc4f-4a83-9e62-b7b7561c64ce",
        "session": 44622
    },
    "tables": [
        {
            "name": "Accounts",
            "schema": "smith",
            "columns": 2,
            "rows": 8
        },
        {
            "name": "simple",
            "schema": "smith",
            "columns": 2,
            "rows": 1053
        },
        {
            "name": "test5",
            "schema": "smith",
            "columns": 1,
            "rows": 23
        }
    ],
    "count": 3
}

   

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

PUT /tables/table

A PUT to a named table will create the table. The payload must be a JSON specification that is an array of columns, each with a name and type field. The table is created using the given columns. Additionally, if you do not specify a column with name _row_id_, then a column of that name is added to the table definition. This column will contain a UUID that uniquely identifies the row across all tables.

The valid types that you can specify in the array of column structure definitions are:

Type Description
string Varying length character string
int Integer value
float32 Real floating point value
float64 Double precision floating point value
bool Boolean value (can only be true or false)

 

The request payload must be a JSON representation of the columns to be created. As an example, this payload creates a table with three columns.

{
    "columns": [
        {
            "name": "first",
            "type": "string",
            "nullable": true
        },
        {
            "name": "id",
            "type": "int",
            "unique": true
        },
        {
            "name": "last",
            "type": "string"
        }
    ]
}

The first column is allowed to have a null value, and the second column must contain unique values.

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

GET /tables/table

If you specify a specific table with the GET operation, it returns JSON payload containing an array of structure, each of which defines the column name, type, size, and nullability.

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

DELETE /tables/table

A DELETE operation to a specific table will delete that table and it's contents from the database, if the current user has delete privilege for that table.

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

PUT /tables/@sql

This is a variation of the previous operation; it allows execution of an arbitrary SQL statement, if the current user has admin privileges. The SQL text to execute must be passed as a JSON-encode string in the body of the request. The reply will either be a rowset or a rowcount object, depending on whether the statement was a select operation (which returns a row set), versus any other statement which just returns a count of the number of rows affected.

For example, here is a request payload that joins two tables and returns a result. Because this is a SQL select statement, the Ego server knows to return a rowset as the result. Otherwise, it returns a rowcount as the result.

    "select people.name, surname.name 
         from \"mary\".\"people\" 
         join \"mary\".\"surname\"
            on people.id == surname.id"

Note that the string must be properly escaped as a JSON string.

 

You can execute multiple statements in a single operation by formatting the payload as an array of strings. For example,

[
    "delete from people where name='Jones'",
    "insert into people(name) values ('Smith')"
]

This executes both statements in order, as a transaction. That is, every statement in the operation must succeed for any of the statements to take effect. If any statement fails, the error response describes the failing statement. When using a transaction of more than one statement, if a SELECT statement is included it must be the last statement in the array.

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

POST /tables/@transaction

This operation allows you to specify an atomic list of operations that must all be successfully performed for the change to occur. That is, it specifies a list of update or delete operations to arbitrary tables, and the changes will be made only if all the changes can be made successfully. This allows the caller to create a transaction of multiple operations. Other users of the same table will only ever see the state of the database before the transaction, or after it is entirely complete. No other user of the database will see a partial version of the update while it is in progress. This is particularly helpful if the client logic requires updates or deletes to multiple tables to reflect a single logical operation, and all the operations must happen together.

The following task operations can be performed as part of a transaction:

Operation Description
delete Delete rows from a table. Specify filters to indicate which rows.
insert Insert a row into a table. Specify values for each column.
readrows Read multiple rows from a table, return as transaction result.
select Read a single row from a table, set symbol values.
symbols Set symbol values for this transaction.
sql Execute arbitrary native SQL statement.
update Update one or more rows with new values. Specify filters to indicate which rows.

 

The payload for a transaction is an array of tasks. Each task has the following members:

Task Item Description
operation The operation to be performed for this particular task.
table The name of the table on which to perform the operation. Not specified for SQL or READROWS operations.
filters If filters are used for this operation, this is an array of filter specifications
columns If subset of the data is to be used, this is an array of the column names to be affected
emptyError A boolean that indicates that the transaction fails if the step does not find or modify any rows
data A representation of a single row, where the object field name is the column name and the object field value is the column value.
sql Optional native SQL string used for "readrows" and "sql" operations only.

If the operation requires multiple filters, those can be individually specified in the filters array; each filter is implicitly joined to the others by an AND() operation, so that all the filters specified must be true for the filter to match a row. For operations that do not specify a filter (such as "INSERT"), the filters list can be empty. For operations are intended to use all the fields of the "data" element, the columns list can be empty. For "DELETE" or "DROP" operations, the data element can be empty or omitted from the payload.

The emptyError boolean value is only used for specific operations: "SELECT", "DELETE", or "UPDATE". By default, if a task for one of these steps does not find any rows (for example, the "DELETE" has a filter that does not match any rows), then the transaction continues. However, if the emptyError flag in the transaction task is set to true, then if no rows are read ("SELECT" operation) or modified ("DELETE" or "UPDATE") then the transaction is stopped with an HTTP 404 ("not found") error. This allows the caller to determine if a transaction is intended to proceed if the given task did not have any effect.

Here is a sample payload with three transactions:

[
    {
        "operation": "insert",
        "table": "x6",
        "data": {
            "address": "123 Elm St",
            "description" : "tx row",
            "first": "Elmer",
            "last": "Fudd",
            "role": "tester"
        }
    },
    {
        "operation": "insert",
        "table": "x6",
        "data": {
            "address": "125 Elm St",
            "description" : "tx row",
            "first": "Daffy",
            "last": "Duck",
            "role": "tester"
        }
    },
    {
        "operation": "update",
        "table":"x6",
        "filters":[
            "EQ(description,'tx row')"
        ],
        "columns": [
            "address"
        ],
        "emptyError": true,
        "data": {
            "address":"666 Scary Drive"
            "description" : "tx row",
            "first": "Daffy",
            "last": "Duck",
            "role": "tester"
        }
    }
]

The first and second tasks insert new data into the table "x6". The third task updates the address of any row that matches the filter of a "description" field equal to "tx row". Note that the third task also explicitly specifies a columns list. This means that even though the data item contains many fields, the only field that will be updated is "address" from the data object.

If the insert fails (perhaps due to a constraint violation, etc.) then no data will be inserted. If the inserts succeed but the update fails (perhaps there is a syntax error in the filter list), then no inserts or updates will occur. If any error occurs, the resulting message indicates how many tasks were processed before the error was encountered, and what the error was.

Note that the third task set the emptyError property to true. In this case, the transaction would fail (and no inserts or updates would have occurred) if the filter expression did not resolve to at least one row.

A successful transaction will return a rowcount object, which has a field "count" which contains the number of rows affected by all the transactions processed.

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

Data Chaining in Transactions

There are two additional task types that can be executed as part of a transaction. These are"

 

Operation Description
select select a row from a table and store the column values in the substitution dictionary for this transaction.
symbol Specify a value for an item that is stored in the substitution dictionary for this transaction.

 

There is a substitution dictionary for each transaction REST API call. This ia a set of key/value pairs. The key is always a name, and the value is any data type. The key name can be specified enclosed in braces to substitute in that value in the operation.

For example, a read of a customer UUID value could then be used to specify the proper filter for that UUID in a subsequent update operation.

Chaining is done by first setting substitution values (either by a select task that reads from a table, or a symbols task that sets values to either a constant or another item in the substitution dictionary). Once the dictionary values are set, they can be referenced in filters, column lists, or data values. The dictionary values are persisted while the entire transaction runs, and then are discarded when the REST call completes.

Here is a simple transaction that reads a value from one table, and inserts it into a second table:

[
    {
        "operation": "select",
        "table": "table1",
        "columns": [
            "customer"
        ],
        "filters": [
            "EQ(key,1)"
        ]
    },
    {
        "operation": "insert",
        "table": "table2",
        "data": {
            "sender": "toby"",
            "recipient": "{{customer}}"
        }
    }
]

The first task is a "SELECT" operation that reads from a table. The select operation will only ever read a single row; there is an implied limit of 1 on the query that reads the table. Because there can only ever be one row, sorting of the result is not needed (or even possible).

If the "columns" array is not given, then a symbol is set for each column in the row retrieved. In this example above, only the "customer" column will be read from the first table.

The second task is an insert into a different table. This table has two columns, "sender" and "recipient", so a value is specified for each column in the new row that is being inserted. Note how the value in the "data" object specifies "{{customer}}" for the object value for the column "recipient". The use of a string with double-braces around a name is the indicator that the value is not the string, but instead is the symbol value "customer" that was read in a previous task. The effect of this is that the "INSERT" task will store the value for customer from the first table in the "recipient" column of the second table.

You might want a case where you read the same table twice, with different filters, and need to keep both values that were read by the select operation. You can do this with a special translation type of symbol. Here is an example transaction that uses the symbol operation:

[
    {
        "operation": "select",
        "table": "table1",
        "columns": [
            "customer"
        ],
        "filters": [
            "EQ(key,1)"
        ]
    },
    {
        "operation": "symbols",
        "data" : {
            "sending_customer" : "{{customer}}"
        }
    },
    {
        "operation": "select",
        "table": "table1",
        "columns": [
            "customer"
        ],
        "filters": [
            "EQ(key,2)"
        ]
    },
    {
        "operation": "symbols",
        "data" : {
            "receiving_customer" : "{{customer}}"
        }
    },
    {
        "operation": "insert",
        "table": "table2",
        "data": {
            "sender": "{{sending_customer}}",
            "recipient": "{{receiving_customer}}"
        }
    }
]

 

In this (contrived) example, there are two separate select operations done on the same table, but with different key values. The first select is looking for a row where "key=1" and the second select is looking for a row where "key=2". These are specified using the filters item in the transaction operation.

After the first select, there is a symbol operation. This moves the value from the "customer" value that was retrieved in the first select into a new symbol, named "sending_customer".

The second select operation is performed, using the different value for "key". This again sets the value of "{{customer}}" to the value in the row for the column named "customer". After this select, a second symbol operation is performed to move the value of "{{customer}}" into a new value, which is named "receiving_customer". In this way, two separate rows were read from the same table (with different filters) and the value for the "customer" column was stored in two different symbol names.

The final step is to use each of these symbol names in an insert operation, which adds a new row to table2 that has columns named "sender" and "recipient" and has the values of the respective "customer" columns, now represented as two different substitution symbols.

   

Rows API

This section covers API functions to

The API for accessing row data in a table uses the /tables/table/rows endpoint name. The result is either a row set (for GET of table rows) or a row count for any other operation that indicates how many rows were affected by the operation. This includes PUT (write rows), PATCH (update rows), and DELETE (delete rows)    

GET /tables/table/rows

This reads the rows from a table. If no parameters are given, then all columns of all rows are returned, in an unpredictable order. The result is a JSON payload containing an array of structures, each of which is a set of fields for each column name, and the value of that field in that row.

The Rows API supports the following parameters on the URL that affect the result set. Additional information about the parameters follows this table:

Parameter Example Description
columns ?columns=id,name Specify the columns to return (if not specified, all columns are returned)
filter ?filter=EQ(name,"TOM") Only return rows that match the filter
limit ?limit=10 Return at most this many rows from the result set
sort ?sort=id Sort the result set by the named column
start ?start=100 Specify the first row of the result set (1-based)

 

You can specify the sort order of the results set by naming one or more columns on which the data is sorted before it is returned to you. Use the sort parameter, with a value which is a comma-separated list of columns. The first column named is the primary sort key, the second column (if any) is the secondary sort key, etc. You can prefix the column name with a tilde ("~") character to make the sort order descending instead of ascending.

You can specify the columns that are to be returned using the columns parameter, with a value that is a comma-separate list of column names. Only those columns are returned in the payload.

You can filter the rows returned using the filter parameter, which contains a filter expression. This consists of an operator, followed by one or two operands in parenthesis. The operands can themselves be filter expressions to create complex expressions. The operators are:

 

Operator Example Description
EQ EQ(id,101) Match rows where the named column has the given value
LT LT(age, 65) Match rows where the named column's value is less than the given value.
LE LE(size,12) Match rows where the named column's value is less than or equal to the given value.
GT LT(age, 65) Match rows where the named column's value is greater than the given value.
GE LE(size,12) Match rows where the named column's value is greater than or equal to the given value.
AND AND(EQ(id,1),EQ(id,2)) Both operands must be true
OR OR(EQ(id,1),EQ(id,2)) Either operands must be true
NOT NOT(EQ(id,101)) Match rows where the operand expression is not true
HAS HAS(foo, 'YES', 'NO') Match rows where character column foo contains "YES" or "NO"
HASALL HASALL(foo, 'YES', 'NO') Match rows where character column foo contains "YES" and "NO"

 

The AND() and OR() operators can contain a list of two or more values. If you specify multiple values, then in the case of AND() the filter is active if all of the sub-expressions are true and in the case of OR() the filter is active if any of the sub-expressions are true.

For the HAS() operator, the first item must be the column name and this is followed by one or more substrings that might be found in the column name; the filter is true if any of the values are present in the column string value. For HASALL(), the parameters are the same as HAS() but the condition is true only if all of the values represented are found in the column string. The HAS() and HASALL() operations are case-sensitive.

Note that in these examples, the value usually being tested is an integer. You can also specify a string value in double quotes, or a floating point value (such as 123.45).

 

The result is called a "rowset" and consists of an object with two values.

Field Description
rows An array of JSON objects, representing a row. The field names are the column names, and the field value are the row values
count An integer value that indicates how many items were returned in the rows array

 

Here is an example output from the call to read rows. This table has two columns defined by the user, called Number and Name. There are two rows in the result set. Note that the result set also includes the synthetic column name _row_id_ which contains a unique identifier for each row in the database.

 

{
    "server": {
        "api": 1,
        "name": "appserver.abc.com",
        "id": "2ef21c8f-cc4f-4a83-9e62-b7b7561c64ce",
        "session": 1525
    },
    "rows": [
    {
        "Name": "Tom",
        "Number": 101,
        "_row_id_": "76d3e219-1015-49c8-9e77-decb750ad13e"
    },
    {
        "Name": "Mary",
        "Number": 102,
        "_row_id_": "a974019e-f9e7-4554-adb4-2004b6f65c03"
    }
    ],
"count": 2
}

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

PUT /tables/table/rows

The PUT method inserts new rows into the table. The payload is either a row descriptor which is a JSON object describing the values of each column in the row to be added, or a rowset which consists of an object with an array of row descriptors. The latter allows an insert of multiple rows at one time. If a column is not specified in the body of the request, the corresponding value in the table is null/zero.

You do not need to specify a _row_id_ item; this will be set for you when the new row is created.

 

Here is an example payload that can be sent to the server to insert a single new row for account number 103 with name "Susan".  

{
    "Name": "Susan",
    "Number": 103
}

 

If the row is successfully inserted, the result is a JSON object with a single field, count which should contain the number 1. _In the future, it will be possible to insert multiple rows in a single call, in which case this value will reflect the number of rows inserted.

You can also send a list of rows that are to be inserted using a rowset. Here is a sample payload that inserts three rows as a single operation:  

{
    "rows" :[
    {
        "Name": "Susan",
        "Number": 103
    },
    {
        "Name": "Timmy",
        "Number": 104
    },
    {
        "Name": "Mike",
        "Number": 105
    }
    ],
    "count": 3
}

Ego allows a variation of an "upsert" operation, which conditionally does an update versus insert on a PUT operation, based on whether the row exists already. This is done using the upsert URL parameter on the API. If the upsert does not have a value, then the upsert is done based on the _row_id_ value. If the upsert parameter has one or more values, these must the column names.

When the upsert option is used, the server determines if one or more rows exist in the table that match the value provided in the row data for the given column(s). If rows already exist, then the operation becomes an UPDATE and the value of any specified column is updated in the row. If there is no such matching row, or if the specified column is not present in the row, then an INSERT operation is done.

This allows the caller to send a payload that is a mix of updates and inserts. For example, any data that was previously read from the database will have a _row_id_ value provided. So if that data is returned on an upsert operation, then those rows will be updated. If the row provided in the request payload does not have a _row_id_ value (because it has not yet been inserted) then an INSERT will be done that includes assigning a row id value. The default is to key the upsert based on the _row_id_ variable, but you can specify alternate variable(s) to use instead based on the logic of your code.

Assume a table FOO with columns for NAME and AGE. A payload that updates one age for an existing row, but inserts a column that has never existed, might look like this:

[
    {
        "NAME": "Susan",
        "AGE": 42,
        "_row_id_": "aboeucy37xhtcs"
    },
    {
        "NAME": "Bob",
        "AGE":  43
    }
]

IF the PUT URL endpoint is ".../FOO/rows?upsert" then the operation will be an upsert based on the row ID column. If the row with the given ID already exists for "Susan", then that row is updated. A row for "Bob" will always be created because it doesn't have a valid row id value yet.

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

PATCH /tables/table/rows

The PATCH method updates existing rows in the table. The payload is a row descriptor (the same as the PUT method) but does not have to specify all the values in the row. Only the values specified in the request body are updated; the other values are left unchanged.

Use the filter parameter to select which row(s) are to be updated:

Parameter Example Description
filter ?filter=EQ(name,"TOM") Only return rows that match the filter
columns ?columns=Id,Name Only update the named columns from the request payload

Note that the use of columns is present to support the case where the client needs to present a model of the entire object represented by the table row, but only wants to update specific values in that model (this can be important for performance when updating an index value, for example). If columns is not specified, then all fields in the request payload are updated.

 

If a _row_id_ field exists in the row representation, then only the matching row in the table with that exact ID will be update. If not specified, all rows will be updated using the same value. In addition to a _row_id_ you can use the filter option to select specific rows that are to be updated. You can reference the column values if they are sufficiently unique.

A common usage is to perform a GET operation on the row(s) you wish to update so you have a rowset with all the IDs already in them. You can then update the value(s) you wish in the rowset, and then pass the rowset back for the PATCH operation to update the values. The presence of the _row_id_ column in the rowset guarantees that only the rows in the rowset are updated, and you may not need any further filtering. Note that in this case, you can still use the ?columns parameter to specify the columns in the rowset that are to be used for the update, and any other column info in the rowset is ignored.

 

Here is an example payload that can be sent to the server to update the row for account number 101 to change the name to "Bob". The account number (and the synthetic row ID) are not modified by this operation. This assumes that only one row in the table has the given account number of 101.

 

{
    "Name": "Bob",
},

 

The url request formed would be something like:

    PATCH https://localhost/tables/Accounts/rows?filter=EQ(Number,101)

This specifies that the row is to be updated (a PATCH method call) and the only row(s) to be updated are those where the Number field is equal to 101. You can also use the _row_id_ variable to specify a specific row, which is always guaranteed to be unique.

When this call runs successfully, the resulting payload is an object with a field count which contains the number of rows that were changed by this operation. A value of zero means the filter did not allow any rows to be modified. You can also use this to verify that you updated one and only one row if you needed the update to be unique to a particular row.

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

DELETE /tables/table/rows

This deletes rows from a table. By default, all rows are deleted. You can use the following parameter to specify which rows are to be deleted:

 

Parameter Example Description
filter ?filter=EQ(name,"TOM") Only delete rows that match the filter

 

If no parameters are given, then all rows are deleted. You can specify the filter parameter to indicate which row(s) are to be deleted from the table. The filter parameter contains a filter expression, of the same form as the GET operation.

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

Permissions

A permissions table is managed by the Ego server that controls whether a given use can read, update, or delete a given table. By default, a user can only set these attributes on tables that they own. An administrator (a user account with "ego.root" privilege) can change the attributes of any table for any user.

You can read the entire list of permissions if you are an admin user.

   

GET /tables/@permissions

This command specifies the pseudo table name @permissions to indicate that the request is to read all the permissions data for all tables. The result is a JSON payload with an array for each permissions object stored in the database, listing the user, schema, table, and a string array of permission names.

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

GET /tables/table/permissions

This command returns a permissions object for the given table and the current user. This includes the user, schema, table, and a string array of permission names.

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

PUT /tables/table/permissions

This command will update the permissions for the given table and the current user. The body of the request must contain a JSON payload with a string array of permission names. The name can start with a "+" in which case the permission is added to the existing data. If the name starts with a "-" then the permission is removed. It is not an error to remove a permission that does not exist.

If there is no existing permission data for the user, a new permissions object is created in the security database for the current user, initialized with the permissions provided.

 

In the event that the REST call returns a non-success status code, the response payload will contain the following diagnostic fields as a JSON payload:

Field Description
status The HTTP status code (integer other than 200)
msg A string with the text of the status message

   

Data Source Names

The /dsns endpoint is also used to create, delete, or manage permissions on data source names. These endpoints are only available to users with root administrator permissions.

GET /dsns/

This endpoint lists the available data source names. For each data source name, the type of database is given, along with any information needed to construct a valid connection string to the database. Passwords, if provided as part of the datasource name, are not returned.

PUT /dsns/

This endpoint creates a new data source name (DSN). The information is stored in the Ego server's DSN datastore. If a password is provided in the DSN definition, it will be encrypted in the back-end store.

GET /dsns/dsn/

This retrieves the information for a single data source name, indicated by dsn in the endpoint pattern.

DEL /dsns/dsn/

This deletes the information for a single data source name, indicated by dsn in the endpoint pattern.

PUT /dsns/dsn/@permissions

This endpoint is used to grant or revoke permissions for the named DSN, indicated by dsn in the endpoint pattern. The JSON payload can either be the definition of a single grant or revoke operation, or can be a list of users and the permissions to grant or revoke.

   

Services

All remaining REST endpoints are provided under the /services path point. Use of this path means that the server will load, compile, and run an Ego program that will respond to the given REST API. This is the mechanism by which the developer can extend the server functionality specific to the needs of the end users.

There are a number of /services endpoints provided in the default installation, and at least one (the /services/admin/logon endpoint) is required for a secure, authenticated server. This section will describe the endpoints provided in the default deployment.

By convention, the EGO_PATH/services directory includes a number of subdirectories to support functions of an Ego web server.

Subdirectory Description
admin Contains services to support administrative and debugging services, such as logon
assets Contains any static resources that are served via the /assets endpoint, such as images
templates Contains static template files (usually) HTML that are used by services.

You can see examples of this by examining the /services/admin/memory endpoint.

  • The code that is loaded and run is in the admin/memory.ego file. This is the primary endpoint name.
  • The code uses a template located in the /templates directory that forms the HTML component of the result
  • The template includes references to read a PNG image from the /assets directory in forming the web page

Example Service Code

Here is the full Ego code for the /services/admin/memory service, found in the "memory.ego" file:

import "http"

func mb(f float64) string {
    return fmt.Sprintf("%3.2fmb", f)
}

func handler( req http.Request, resp http.Response ) {
    // Prepare the data to be used by the page.
    m := util.Memory()
    h := os.Hostname()

    pageData := { 
        Allocated: mb(m.current),
        Total:     mb(m.total),
        System:    mb(m.system),
        GC:        m.gc,
        ID:        _instance,
        Date:      time.Now().Format(time.RFC1132)),
        Host:      h,
    }

    // Given a path to the template asset, write the page using the
    // dynamically generated data
    resp.WriteTemplate("lib/services/templates/memory.html", pageData)
}

The service always calls the handler entrypoint, and always passes in the request and response objects. The services uses the built-in util.Memory() function to get information about the system memory usage. It then extracts the data it wants to send to the user, including using the local mb() function defined in the service file to format bytes as megabytes.

Finally, the code uses the resp.WriteTemplate() function to reference a template file, and provide the values that are to be plugged into the template file. The template processor reads the template file, performs any substitutions in the template from the supplied data structure (so that a reference to {{.Total}} in the template is replaced with the value of pageData.Total from the supplied data structure).

Example Template File

The template contains the actual HTML text that will be sent back as the response to the query (via resp.WriteTemplate() in the service code).

The template contains both static text, and substitution operators, which are identified by being enclosed in double-braces, such as {{.Total}} which is a substitution operator for a field named Total in the data structure supplied with the template. This allows the template to contain the design/formatting code needed to present the desired page, while variable values can be injected as part of the template processing.

Here is the associated template file, located in lib/services/templates/memory.html:

<!DOCTYPE html>
<!-- Demo web page dynamically rendered by a service. -->
<html>
    <head>
        <title>Ego Memory ({{.Host}})</title>
        <style>
            table,
            td,
            th {
                border: none;
                width: 400px;
                border-collapse: collapse;
            }
        </style>
    </head>

    <body>
        <table style="border: none;width: 440px;border-collapse:collapse">
            <tr>
                <td>
                    <!-- The asset must have a root path of /assets to be located properly -->
                    <img src="/assets/logo.png" alt="Ego logo" style="width:300px;height:150px;">
                </td>
                <td>
                    <h1>&nbsp;&nbsp; Memory Statistics</h1>
                </td>
            </tr>
        </table>
        <br> <br>{{.Date}}
        <p>
            <table>
                <tr>
                    <td>Currently allocated</td>
                    <td style="text-align: right">{{.Allocated}}</td>
                </tr>
                <tr>
                    <td>Total allocated</td>
                    <td style="text-align: right">{{.Total}}</td>
                </tr>
                <tr>
                    <td>System memory</td>
                    <td style="text-align: right">{{.System}}</td>
                </tr>
                <tr>
                    <td>Garbage collections</td>
                    <td style="text-align: right">{{.GC}}</td>
                </tr>
            </table>
            <br> Server {{.Host}}, session {{.ID}}
            <br>
    </body>
</html>

Note the references to substitution operators throughout the page, showing where the text of the service data structure items are injected into the HTML page that is sent back to the caller.

Also note that there is a reference to an image via an img src="..." tag. This will cause the web browser presenting the HTML to make a second call to the Ego web server to retrieve the image from the assets directory on the web server.