UNPKG

@expressive-analytics/deep-thought-service

Version:

Typescript conversion of Deep Thought Services (formerly providers)

559 lines (558 loc) 21 kB
"use strict"; 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;