Server

This example demonstrates a REST Server capabale of running asynchronous query jobs Jobs are run in worker processes that will execute arbitrary q-sql and trigger a callback when done A REST Client is expected to poll for async job results.

Resources for the server are organized as a list of Projects containiing lists of databases Databases are organized as a list of tables

Usage Patterns are:

GET /v1/hc                                                             Simply health check
GET /v1/projects                                                       List all projects
GET /v1/projects/{projectID}                                           List one project's attributes
GET /v1/projects/{projectID}/databases                                 List all databases for projectID
GET /v1/projects/{projectID}/databases/{databaseID}                    List one database's arrtibutes
GET /v1/projects/{projectID}/databases/{databaseID}/tables             List all tables for projectID + databaseID
GET /v1/projects/{projectID}/databases/{databaseID}/tables/{tableID}   List table arributes
POST /v1/projects                                                      Create a new project
POST /v1/projects/{projectID}                                          Update a project (rename/delete)
POST /v1/projects/{projectID}/databases                                Create a database
POST /v1/projects/{projectID}/databases/{databaseID}                   Update a database
POST /v1/projects/{projectID}/databases/{databaseID}/tables            Create a table
POST /v1/projects/{projectID}/databases/{databaseID}/tables/{tableID}  Update a table
GET /v1/projects/{projectID}/jobs/                                     List all running queries
POST /v1/projects/{projectID}/jobs/                                    Run a new query
GET /v1/projects/{projectID}/jobs/{jobID}                              Check on the status of a job
GET /v1/projects/{projectID}/jobs/{jobID}/results                      Get the JSONified query results
.rest:.com_kx_rest / Make an alias for convenience
.rest.init[];

// Querystring Params
\d .demo
param.projectID:.rest.reg.data[`projectID;-7h;1b;0;"Project ID"];
param.databaseID:.rest.reg.data[`databaseID;10h;1b;"";"Database ID"];
param.tableID:.rest.reg.data[`tableID;10h;1b;"";"Table ID"];
param.jobID:.rest.reg.data[`jobID;-7h;1b;0;"Job ID"];

// Body Params
.rest.reg.object[`project;
    .rest.reg.data[`name;-11h;0b;`;"Project name"],
    .rest.reg.data[`dir;10h;0b;"";"Project directory"]];
.rest.reg.object[`database;
    .rest.reg.data[`name;-11h;0b;`;"Database name"]];
.rest.reg.object[`table;
    .rest.reg.data[`name;-11h;0b;`;"Table name"],
    .rest.reg.data[`table;98h;0b;([] x:());"Table content"]];
.rest.reg.object[`job;
    param.databaseID,
    .rest.reg.data[`query;10h;0b;"";"q query"]];

param.project:.rest.reg.body[`project;0b;::;"Project information"];
param.database:.rest.reg.body[`database;0b;::;"Database information"];
param.table:.rest.reg.body[`table;0b;::;"Table information"];
param.job:.rest.reg.body[`job;0b;::;"Job information"];

.rest.register[`get;"/v1/hc";"simple healthcheck";{"ok"};()!()];

// GETTER API
.rest.register[`get;"/v1/projects";
    "List all projects";
    {.demo.projects};
    ::];
.rest.register[`get;"/v1/projects/{projectID}";
    "List one projects attributes";
    {first select from .demo.projects where id = x[`arg;`projectID]};
    param.projectID];
.rest.register[`get;"/v1/projects/{projectID}/databases";
    "List all databases for a project";
    {.demo.findDB};
    param.projectID];
.rest.register[`get;"/v1/projects/{projectID}/databases/{databaseID}";
    "List a databases attributes";
    {
        db:.demo.findDB x;
        enlist[`name]!enlist db `h
        };
    param.projectID,param.databaseID];
.rest.register[`get;"/v1/projects/{projectID}/databases/{databaseID}/tables";
    "List all tables in a database";
    {
        db:.demo.findDB x;
        key db `h
        };
    param.projectID,param.databaseID];
.rest.register[`get;"/v1/projects/{projectID}/databases/{databaseID}/tables/{tableID}";
    "List table attributes";
    {'`notImplemented};
    param.projectID,param.databaseID,param.tableID];

// CREATE APIs
.rest.register[`post;"/v1/projects";
    "Create a new project";
    {
        system "mkdir -p ",di:projRoot,x[`data;`dir];
        .demo.projects,:`name`id`dir`created!(x[`data;`name];count .demo.projects;di; .z.p);
        :last .demo.projects
        };
    param.project];
.rest.register[`post;"/v1/projects/{projectID}";
    "Update a project";
    {'`notImplemented};
    param.projectID];
.rest.register[`post;"/v1/projects/{projectID}/databases";
    "Add a database to a project";
    {
        proj:.demo.findProject x;
        dbh:hsym `$db:proj[`dir],"/",string n: x[`data;`name];
        if[() ~ key dbh; system "mkdir -p ",db];
        :enlist[`id]!enlist n
        };
    param.projectID,param.database];
.rest.register[`post;"/v1/projects/{projectID}/databases/{databaseID}";
    "Rename or delete a database";
    {"not implemented"};
    param.projectID,param.databaseID];
.rest.register[`post;"/v1/projects/{projectID}/databases/{databaseID}/tables";
    "Add a table to a database";
    { .demo.writePar x };
    param.projectID,param.databaseID,param.table];
.rest.register[`post;"/v1/projects/{projectID}/databases/{databaseID}/tables/{tableID}";
    "Add or overwrite dates in a table";
    {`notImplemented};
    param.projectID,param.databaseID,param.tableID];

// JOB API
.rest.register[`get;"/v1/projects/{projectID}/jobs";
    "List all jobs for a project";
    {select from .demo.jobs where projectID = x[`arg; `projectID];};
    param.projectID];
.rest.register[`post;"/v1/projects/{projectID}/jobs";
    "Submit a query job";
    {
        proj:findProject x;
        avail:first .demo.workers except exec worker from .demo.jobs;
        db:proj[`dir],"/",x[`data;`databaseID];
        neg[avail] (`.demo.runQuery; db; x[`data;`query]);
        .demo.jobs,:`id`worker`projectID`status!(count .demo.jobs;
            avail;proj `id;`active);
        :last .demo.jobs
        };
    param.projectID,param.job];
.rest.register[`get;"/v1/projects/{projectID}/jobs/{jobID}";
    "List details of a job";
    {findJob x};
    param.projectID,param.jobID];
.rest.register[`get;"/v1/projects/{projectID}/jobs/{jobID}/results";
    "Get results for a finished job";
    {
        job:select from findJob[x] where status=`done;
        if[1 <> count job;'"Job not finished"];
        job[`worker] ".demo.results"
        };
    param.projectID,param.jobID];

projRoot:system["cd "],"/exampleProjects/";
projects:([] name:(); id:"j"$(); dir:(); created:"p"$());
findProject:{
    proj:select from .demo.projects where id = x[`arg;`projectID];
    if[0 = count proj; '"No such project ", x[`arg, `projectID]];
    :first proj;
    }
findDB:{
    proj:findProject x;
    dbh:hsym `$db:proj[`dir],"/",n: x[`arg;`databaseID];
    if[() ~ key dbh;'"Database does not exist: ", n];
    :`name`h!(n;dbh)
    }
findJob:{
    select from .demo.jobs where projectID = x[`arg;`projectID],
        id = x[`arg;`jobID]
    }
writePar:{
    db:findDB x;
    name:x[`data;`name];
    t:x[`data;`table];
    t[`date]:"D"$t `date;
    a:cols[t] except `date;
    kt:?[t;();`date;a!a];
    (`$string key[kt]) {[root;name;date;d]
        (` sv root,date,name,`) set flip d
        }[db `h;name]' value kt;
    :name
    };
done:{ .demo.jobs:update status:`done from .demo.jobs where worker = .z.w;};
maxWait:00:00:05;
i:0;
n:10;
workers:();
jobs:([] id:"j"$(); worker:"I"$(); projectID:"j"$(); status:`$());
\d .

.z.ph:{.rest.process[`GET;x]}
.z.pp:{.rest.process[`POST;x]}
.z.po:{.demo.i+:1;}
.z.ts:{[start;now]
    if [now > start + .demo.maxWait;
        -2 "Took longer than ",string[.demo.maxWait],
            " to start ",string[.demo.n]," workers";
        -2 "Exiting...";
        exit 1];
    // Clear timer and uninstall .z.po
    if[.demo.n = count .z.W;
        system "t 0";
        .z.po:{};
        .demo.workers:key .z.W];
    }[.z.p;];

do[10; system "q queryworker.q -server ",string system "p"];
\t 1000