@expressive-analytics/deep-thought-service
Version:
Typescript conversion of Deep Thought Services (formerly providers)
559 lines (558 loc) • 21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DTService = exports.DTHTTPError = void 0;
const DTSession_1 = require("./DTSession");
const DTBasicVerifier_1 = require("./DTBasicVerifier");
const deep_thought_js_1 = require("@expressive-analytics/deep-thought-js");
class DTHTTPError extends Error {
constructor(code, header, message) {
super(message);
this.code = code;
this.header = header;
}
}
exports.DTHTTPError = DTHTTPError;
function empty(val) {
return val === undefined || val === null || val === "";
}
class DTService {
constructor(verifier) {
/**
* determines which automatic actions are available, defaults to "read-only"
* valid values include: get, list, count, create, create_many,
* update, update_many, upsert, remove, remove_all
*/
this.automatic_actions = [];
this.lookup_column = "id"; // unique "key" column for automatic actions, defaults to model's primary key
this.verifier = (verifier === undefined) ? new DTBasicVerifier_1.DTBasicVerifier() : verifier;
this.session = DTSession_1.DTSession.shared();
if (this.$meta_model !== undefined) {
// try to instantiate the model via meta
/// @TODO implement the meta
// me.model = ...
}
}
actionSessionDestroy() {
DTSession_1.DTSession.destroy();
}
setParams(params) {
this.params = new deep_thought_js_1.DTParams(params, this.db);
}
get model() {
return this.$model;
}
set model(val) {
this.$model = val;
// make sure we have populated the lookup column
if (this.lookup_column === undefined)
this.lookup_column = this.$model.primaryKeyColumn();
}
//==================
//! Request Handling
//==================
/** @name Request Handling
* Session setup and action delivery
*/
///@{
/**
* A convenience method for handling a standard request and sending a response
*/
handleRequest(req, rsp) {
if (this.model === undefined)
throw new Error(`'${this.constructor.name} is missing model definition.`);
this.response = rsp;
this.request = req;
this.setParams(req.params);
const act = this.params.stringParam("act");
this.params.unset("act");
const action = act === undefined || act == "" ? '' : "action" + act.replace(/\w\S*/g, (txt) => {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
try {
if (this.verifyRequest(action))
this.performAction(action);
this.response.respond(this.params.allParams());
}
catch (e) {
if (e instanceof DTHTTPError) {
this.response.sendHeader("HTTP/1.0 442 DTR Application Error");
this.setResponseCode(-1);
this.response.respond("Request could not be verified.");
}
}
}
verifyRequest(action) {
const token = this.params.stringParam("dttok");
this.params.unset("dttok");
const status = this.verifier.verify(action, token);
this.db = this.verifier.db();
return status;
}
/**
* performs an action by name
* @param action - the action to perform (uses 'act' param key, if null)
* @NOTE if action is the name of a method, the appropriate method is called
*/
performAction(action) {
action = (action !== undefined ? action : this.params.stringParam("act"));
if (this.actionExists(action)) {
this.setResponse(this[action]());
this.recordRequest(action);
}
else
console.warn("Action not found ({action}).");
}
setResponse(response) {
this.response.setResponse(response);
}
responseCode() {
return this.response.error();
}
/** @return returns null so that it may be chained with an action's return statement */
setResponseCode(code) {
this.response.error(code);
return null;
}
///@}
recordRequest(action) {
// override in subclasses to customize logs
}
/** @internal */
actionExists(action) {
try {
if (typeof this[action] === 'function')
return true;
}
catch (e) { }
return false;
}
//====================
//! Automatic Actions
//====================
/** @name Automatic Actions
* Default behavior for providers with a defined model
*/
///@{
/**
* get a MODEL
* @http_method get
* @param object dtf a querybuilder filter
* @return ENTITY_MODEL returns an object
*/
actionGet() {
if (!this.isAutomatic("get")) {
this.response.error(403);
return "unauthorized action (get)";
}
const me = this.constructor;
const model = this.$entity_model !== undefined ? this.$entity_model : this.model;
let filter = this.enforce(this.advancedFilterBy(this.db.filter()), "get", model);
const order = this.params.stringParam("dt_order");
if (order !== undefined) {
const desc = this.params.boolParam("dt_desc", false);
filter = this.orderBy(filter, [{ order: order, desc: desc }]);
}
else {
const dto = this.params.arrayParam("dto");
filter = this.orderBy(filter, dto);
}
try {
return model.query(filter);
}
catch (e) {
this.response.error(404); //404 Not Found
}
return null;
}
/**
* get a random set of MODEL objects
* @http_method GET
* @param integer draws the number of objects to return
* @param string wt_col the column or value to use for weighing picks
* @param integer seed a seed for the random generator
* @param object dtf a querybuilder filter
* @return array[MODEL] returns a list of random objects
*/
actionPick() {
if (!this.isAutomatic("pick")) {
this.response.error(403);
return "unauthorized action (pick)";
}
const model = this.model;
const filter = this.enforce(this.advancedFilterBy(this.db.filter()), "pick", model);
return model.pick(this.params.intParam("draws", 1), filter, this.params.intParam('wt_col', 1));
}
/**
* get a list of MODEL objects
* @http_method get
* @param object dtf a querybuilder filter
* @param integer dt_ct number of records per page, triggers pagination mode
* @param integer dt_pg page number for paginated results, triggers pagination mode
* @param string dt_order the column to order by
* @param boolean dt_desc determines whether records are in descending order
* @param boolean dt_proxy determines whether has-many properties are expanded
* @param string dt_q a basic search string
* @param string dt_col the column to apply the basic search string dt_q
* @return array[MODEL] returns a list of objects, possibly paginated
*/
actionList() {
if (!this.isAutomatic("list")) {
this.response.error(403);
return "unauthorized action (list)";
}
this.response.as_proxy = typeof this.params["dt_proxy"] !== 'undefined' ? this.params.boolParam("dt_proxy") : true;
const model = this.model;
let filter = this.db.filter();
const like = this.params.stringParam("dt_q");
if (!empty(like))
filter = this.filterBy(filter, this.params.stringParam("dt_col", "name"), like);
filter = this.advancedFilterBy(filter);
const order = this.params.stringParam("dt_order");
if (!empty(order)) {
const desc = this.params.boolParam("dt_desc", false);
filter = this.orderBy(filter, [{ order, desc }]);
}
else if (typeof this.params["dto"] !== 'undefined') {
const dto = this.params.arrayParam("dto");
filter = this.orderBy(filter, dto);
}
else // MSSQL requires order-by for limit
filter = this.orderBy(filter, [{ order: model.primaryKeyColumn(), desc: false }]);
if (!empty(this.archive_column)) {
let p = {};
p[`${model.dtType()}.${this.archive_column}`] = 0;
filter.filter(p);
}
this.enforce(filter, "list");
const ct = this.params.intParam("dt_ct");
if (ct > 0) { // dt_pg is set, we are paginating
const page = this.params.intParam("dt_pg", 1) - 1;
return {
"total": model.count(filter),
"items": model.select(filter.limit(ct, page * ct))
};
}
else
return model.select(filter, model.dtType() + ".*");
}
/** override to customize simple filter behavior */
filterBy(qb, col, str) {
const model = this.model;
const matches = "{this.db.ilike} '%{str}%'";
let alias = model.aliasForOwner(col);
if (empty(alias))
alias = model.dtType();
return qb.where(`${alias}.${col} ${matches}`);
}
/** override to customize advanced filter behavior */
advancedFilterBy(qb) {
const dtf = this.params.objectParam("dtf");
const m = this.model;
return m.filter(qb, dtf);
}
enforce(qb, action, model) {
if (!empty(model))
model = this.model;
switch (action) {
case "get":
case "create":
case "update":
case "upsert":
const id1 = this.params.stringParam(this.lookup_column);
if (!empty(id1)) {
let p = {};
p[`${model.dtType()}.${this.lookup_column}`] = id1;
qb.filter(p);
}
break;
case "remove":
const id = this.params.stringParam(this.lookup_column);
if (!empty(id)) {
let p = {};
p[`${this.lookup_column}`] = id;
qb.filter(p);
}
break;
case "remove_many":
const ids = this.params.arrayParam(this.lookup_column);
if (ids.length > 0) {
let p = {};
p[`${model.dtType()}.${this.lookup_column}`] = ["IN", ids];
qb.filter(p);
}
break;
}
return qb;
}
/**
* @param order an array of order/desc pairs
* @param desc string determines whether to sort in descending order
* override to customize sorting behavior
* @return returns the query builder with a new orderBy setting
*/
orderBy(qb, order) {
if (order === undefined)
return qb;
Object.entries(order).forEach(([i, o], index) => {
qb.appendOrderBy(this.orderCol(qb, o["order"], i), typeof o["desc"] !== 'undefined' ? o["desc"] : "ASC");
});
return qb;
}
orderCol(qb, order, uniq) {
const path = order.split(".");
const model = this.model;
let m = model;
let alias = m.aliasForOwner(order);
if (!alias)
alias = m.dtType();
let col = path.pop();
path.forEach((p, i) => {
alias = m.join(qb, p, alias, "ob{uniq}");
let x = m.modelFor(p);
if (x instanceof deep_thought_js_1.DTModel)
m = x;
else
m = x[0];
});
// we can pass an equation if we use the = prefix
// this is not a good plan, cause injection
//if(substr(col,0,1)=="=")
// return substr(col,1);
const esc = qb.db.constructor.col_esc();
return `${alias}.${esc}${col}${esc}`;
}
/**
* get a count of MODEL objects
* @http_method GET
* @param object dtf a querybuilder filter
* @return integer returns the total count of matching objects
*/
actionCount() {
if (!this.isAutomatic("count")) {
this.response.error(403);
return "unauthorized action (count)";
}
const model = this.model;
let filter = this.enforce(this.db.filter(), "count");
if (!empty(this.archive_column)) {
let p = {};
p[`${model.dtType()}.${this.archive_column}`] = 0;
filter.filter(p);
}
filter = this.advancedFilterBy(filter);
return model.count(filter);
}
/**
* create a new MODEL
* @http_method POST
* @param ENTITY_MODEL any valid model properties
* @return ENTITY_MODEL the newly created object
*/
actionCreate() {
if (!this.isAutomatic("create")) {
this.response.error(403);
return "unauthorized action (create)";
}
const model = !empty(this.$entity_model) ? this.$entity_model : this.model;
const qb = this.enforce(this.db.qb().fail(), "create", model);
const params = this.params.allParams();
let creation_params = {};
if (!empty(this.creation_column)) // use the creation timestamp
creation_params[this.creation_column] = this.db.constructor.now();
if (!empty(this.modified_column)) // use the modified timestamp
params[this.modified_column] = this.db.constructor.now();
return model.upsert(qb, params, creation_params);
}
/**
* create multiple MODEL objects
* @http_method PUT
* @param array a list of objects to create
*/
actionCreateMany() {
if (!this.isAutomatic("create_many")) {
this.response.error(403);
return "unauthorized action (create_many)";
}
const params = this.params.params; //get raw, dirty params
for (let p of params) {
this.setParams(p);
this.actionCreate();
}
this.setParams(params); //restore the original params
}
/**
* update a MODEL
* @http_method POST
* @param ENTITY_MODEL a subset of properties of the model to update
* @return ENTITY_MODEL returns the updated object, or 404 if not found
*/
actionUpdate() {
if (!this.isAutomatic("update")) {
this.response.error(403);
return "unauthorized action (update)";
}
const model = !empty(this.$entity_model) ? this.$entity_model : this.model;
try {
let filter = this.enforce(this.db.filter(), "update", model);
const params = this.params.allParams();
if (!empty(this.modified_column)) // use the modified timestamp
params[this.modified_column] = this.db.constructor.now();
// this should fail if we don't exist
const obj = model.query(filter);
obj.clean();
obj.merge(params);
obj.update();
const proxy = typeof this.params["dt_proxy"] !== 'undefined' ? this.params.boolParam("dt_proxy") : true;
return model.byID(this.db, obj[model.primaryKeyColumn()]);
}
catch (e) {
this.response.error(404);
console.warn(e.message);
return "record does not exist";
}
}
/**
* update many MODEL objects
* @http_method POST
* @return array[ENTITY_MODEL] returns the updated objects
*/
actionUpdateMany() {
if (!this.isAutomatic("update_many")) {
this.response.error(403);
return "unauthorized action (update_many)";
}
const params = this.params.params; //get raw, dirty params
let objs = [];
for (let p of params) {
this.setParams(p);
objs.push(this.actionUpdate());
}
this.setParams(params); //restore the original params
return objs;
}
/**
* update or create a MODEL, as necessary
* @http_method PATCH
* @return MODEL returns the existing or newly created object
*/
actionUpsert() {
if (!this.isAutomatic("upsert")) {
this.response.error(403);
return "unauthorized action (upsert)";
}
const model = this.model;
const f = typeof this.params["dtf"] !== 'undefined'
? this.advancedFilterBy(this.db.filter())
: this.db.filter(this.params.allParams());
const filter = this.enforce(f, "upsert", model);
let params = this.params.allParams();
let creation_params = {};
if (!empty(this.creation_column)) // use the creation timestamp
creation_params[this.creation_column] = this.db.constructor.now();
if (!empty(this.modified_column)) // use the modified timestamp
params[this.modified_column] = this.db.constructor.now();
return model.upsert(filter, params, creation_params);
}
/**
* loop through the param +dt_items+ and upsert
* @http_method PATCH
* @return array[MODEL] returns the upserted list
*/
actionUpsertMany() {
if (!this.isAutomatic("upsert_many")) {
this.response.error(403);
return "unauthorized action (upsert_many)";
}
const model = this.model;
let creation_params = {};
const now = this.db.constructor.now();
if (!empty(this.creation_column)) // use the creation timestamp
creation_params[this.creation_column] = now;
const arr = this.params.arrayParam("dt_items");
let out = [];
for (let params of arr) {
const f = model.filter(this.db.filter(), params);
let filter = this.enforce(f, "upsert", model);
if (!empty(this.modified_column)) // use the modified timestamp
params[this.modified_column] = now;
out.push(model.upsert(filter, params, creation_params));
}
return out;
}
/**
* remove a MODEL
* @http_method DELETE
* @param integer id the record to remove
*/
actionRemove() {
if (!this.isAutomatic("remove")) {
this.response.error(403);
return "unauthorized action (remove)";
}
const model = this.model;
const filter = this.enforce(this.advancedFilterBy(this.db.filter()), "remove", model);
if (!empty(this.archive_column)) {
let p = {};
p[`${this.archive_column}`] = 1;
model.updateRows(filter, p);
}
else
model.deleteRows(filter);
}
/**
* remove multiple MODEL
* @http_method DELETE
* @param array the list of records to delete
*/
actionRemoveMany() {
if (!this.isAutomatic("remove_many")) {
this.response.error(403);
return "unauthorized action (remove_many)";
}
const model = this.model;
const filter = this.enforce(this.advancedFilterBy(this.db.filter()), "remove_many", model);
if (!empty(this.archive_column)) {
let p = {};
p[`${this.archive_column}`] = 1;
model.updateRows(filter, p);
}
else
model.deleteRows(filter);
}
///@}
static factory(p, db, verifier) {
const cls = (typeof p["class"] === 'undefined' ? this : p["class"]);
if (db === undefined)
db = deep_thought_js_1.DTStorage.defaultStore();
let svc;
if (typeof p["verifier"] === 'undefined')
svc = new cls(verifier);
else
svc = new cls(new p["verifier"](db));
if (typeof p["model"] !== 'undefined')
svc.model = p["model"];
let actions = [];
if (typeof p["access"] !== 'undefined') {
if (p["access"].indexOf("r") > -1)
actions = [...actions, ...["get", "list", "count", "pick"]];
if (p["access"].indexOf("w") > -1)
actions = [...actions, ...["create", "update", "upsert", "remove"]];
}
if (svc.automatic_actions.length == 0) // if we haven't specified...
svc.automatic_actions = actions;
if (!empty(p["creation_column"]) && empty(svc.creation_column))
svc.creation_column = p["creation_column"];
if (!empty(p["modified_column"]) && empty(svc.modified_column))
svc.modified_column = p["modified_column"];
if (!empty(p["archive_column"]) && empty(svc.archive_column))
svc.archive_column = p["archive_column"];
if (!empty(p["lookup_column"]) && empty(svc.lookup_column))
svc.lookup_column = p["lookup_column"];
svc.$public = p["type"];
return svc;
}
isAutomatic(action) {
return this.automatic_actions.indexOf(action) > -1;
}
}
exports.DTService = DTService;