Customer

Sample REST server demonstrating various styles of REST endpoints:

  • Static, where resource names are predefined at endpoint registration time {/customers}.
  • Dynamic, where the resource name (or part of it) is supplied at call-time (e.g. /db/{tbl}/{col}.
  • API-like: same as static but uses verbs instead of names (e.g. /getCustomers)
//
// @desc App initialization.
//
init:{
    .rest.init[];               / Initialize REST framework
    .rest.brkOnErr: $[first[string .z.o]in"ml";$[()~@[system;"tty -s";0b];{[]1b};{[]0b}];{[]'`nyi}][]; // Break on error when attached to console

    initStatic[];               / Sample static endpoints
    initDynamic[];              / Sample dynamic endpoints
    initApi[];                  / Sample API-like endpoints

    .rest.register[`get;"/help"; / Returns information about registered endpoints
        "Retrieves information about registered REST endpoints";
        {.rest.t};
        ()!()];

    .rest.register[`get;"/hc"; / health-check
        "health-check endpoint";
        {"ok"};
        ()!()];

    .rest.register[`get;"/_ping";
        "for testing";
        {"pong"};
        ()!()];

    //
    // Hook request processing.
    //
    req:{b,::enlist y; .h.hn . .rest.process[x;y]};
    .z.ph:req[`GET];
    .z.pp:req[`POST];
    }

//
// Sample parameter subset to support paging.
//
pagingParams:.rest.input.param[`i;-6h;0b;0;"Offset of first row"],
    .rest.input.param[`cnt;-6h;0b;10;"Number of rows to return"];

//
// Wraps `#` to limit to data size.
//
take:{[n;d]min[(n;count d)]#d}

//
// Initialization examples.
//

//
// Static endpoints, and various styles of versioning.
//
initStatic:{
    .rest.register[`get;"/customers";
        "Returns all customers";
        {take[x[`arg;`cnt]]select from customers where i>=x[`arg;`i]};
        pagingParams
        ];

    .rest.register[`get;"/customers.2";
        "Returns all customers (version 2)";
        {take[x[`arg;`cnt]]update newCol:1 from select from customers where i>=x[`arg;`i]};
        pagingParams
        ];

    .rest.register[`get;"/v3/customers";
        "Returns all customers (version 3)";
        {take[x[`arg;`cnt]]update newCol:1, newCol2:10 from select from customers where i>=x[`arg;`i]};
        pagingParams
        ];

    .rest.register[`get;"/customers/{id}";
        "Returns one or more customers by their IDs";
        {select from customers where id in x[`arg;`id]};
        .rest.input.param[`id;6h;1b;0Ni;"One or more customer IDs"]
        ];

    //! the following is work in progress

    / .rest.template[`newCustomer;
    /   rest.input.param[`name;-11h;`one;`;"Customer name"],
    /   rest.input.param[`loc;10h;`required`nullable;`;"Customer location"]
    /   ];

    / .rest.template[`newCustomer;
    /   rest.input.param[`name;-11h;"Customer name";`flags`defVal!(`one;)],
    /   rest.input.param[`loc;10h;`one;`;"Customer location"]
    /   ];

    //! concept of flags to capture both requirement, and vector-ness:
    //! zeroOrOne
    //! zeroOrMany
    //! one
    //! oneOrMany
    //! nullable?

    / .rest.register[`post;"/customers";
    /   "Creates one or more customers";
    /   {break;};
    /   .rest.input.param[`id;6h;1b;0Ni;"One or more customer IDs"],
    /   .rest.input.param[`details;`newCustomer;`zeroOrMany;::;"One or more customer objects"],
    /   .rest.input.header[`$"custom-header";6h;1b;0Ni;"One or more customer IDs"],
    /   .rest.input.body[`newCustomer;`oneOrMany;::;"One or more customer object"]
    /   ];

    / .rest.register[`put;"/customers/{id}";
    /   "Update one or more customers by their IDs";
    /   {brk2};
    /   .rest.input.param[`id;6h;1b;0Ni;"One or more customer IDs"]
    /   ];

    }

initDynamic:{
    .rest.register[`get;"/db";
        "Retrieves list of table names";
        {tables[]};
        ()!()
        ];

    .rest.register[`get;"/db/{table}";
        "Retrieves a table";
        .tbl.getData;
        .rest.input.param[`table;-11h;1b;`;"Table name"],
        pagingParams
        ];

    .rest.register[`get;"/db/{table}/meta";
        "Retrieves metadata of a  table";
        {0!meta x[`arg;`table]};
        .rest.input.param[`table;-11h;1b;`;"Table name"]
        ];

    .rest.register[`get;"/db/{table}/{col}";
        "Retrieves a column subset of a table";
        .tbl.getData;
        .rest.input.param[`table;-11h;1b;`;"Table name"],
        .rest.input.param[`col;11h;1b;`;"Result columns"],
        pagingParams
        ];

    / .rest.register[`post;"/db/query"; / Perform a dynamic query
    /   "Queries a column subset of a table where the POST body is the where-clause";
    /   .tbl.getData;
    /   ()!()
    /   ];
    }


initApi:{
    .rest.register[`get;"/getCustomers";
        "Returns all customers";
        {([] id:til 10; nm:10?`5)};
        ::
        ];

    }

//
// Automatic data retrieval.
//

//
// @desc Generic data retrieval handler. Look for {tbl}, {id}, and {rel} arguments
//
.tbl.getData:{
    tn:x[`arg;`table];
    i_:$[`i in key x[`arg];x[`arg;`i];0];
    cnt:$[`cnt in key x[`arg];x[`arg;`cnt];0W];

    if[not tn in tables[]; 'table]; / Check whether table exists
    w:$[""~0N!flt:x[`data];();enlist parse flt]; / If request is a POST, then data is expected to be the where clause
    c:$[`col in key x`arg;{x!x}x[`arg;`col];()]; / Columns to retrieve
    take[cnt]select from ?[tn;w;0b;c] where i>=i_
    }

{
    init[];
    }[]

//
// Test data
//
n:100
customers:([] id:1+til n; name:n?`7)
sn:`aapl`goog`msft`nke`ftse
symbols:1!([] sym:`aapl`goog`msft`nke`ftse; src:count[sn]?`8);
trades:([] ts:$[12h;.z.d]+$[`minute;1]*til n;
    sym:n?(0!symbols)[`sym];
    price:n?10.0;
    size:n?1000)