UNPKG

ananse

Version:

Ananse is a lightweight NodeJs framework with batteries included for building efficient, scalable and maintainable USSD applications.

1,640 lines (1,615 loc) 61.7 kB
var __defProp = Object.defineProperty; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __knownSymbol = (name, symbol) => { return (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name); }; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __accessCheck = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet = (obj, member, getter) => { __accessCheck(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet = (obj, member, value, setter) => { __accessCheck(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var __forAwait = (obj, it, method) => (it = obj[__knownSymbol("asyncIterator")]) ? it.call(obj) : (obj = obj[__knownSymbol("iterator")](), it = {}, method = (key, fn) => (fn = obj[key]) && (it[key] = (arg) => new Promise((yes, no, done) => (arg = fn.call(obj, arg), done = arg.done, Promise.resolve(arg.value).then((value) => yes({ value, done }), no)))), method("next"), method("return"), it); // src/types/request.ts import { ServerResponse } from "http"; var Request = class { constructor(_url2, req) { this.method = req.method; this.path = _url2.pathname; this.url = _url2.href; this.headers = req.headers; this.query = _url2.query; } }; var Response = class extends ServerResponse { }; // src/core/app.core.ts import { createServer } from "http"; import { parse } from "url"; // src/helpers/constants.ts var SupportedGateway = /* @__PURE__ */ ((SupportedGateway2) => { SupportedGateway2["wigal"] = "wigal"; SupportedGateway2["emergent_technology"] = "emergent_technology"; return SupportedGateway2; })(SupportedGateway || {}); var MAXIMUM_CHARACTERS = 182; // src/gateways/base.gateway.ts var Gateway = class { constructor(request, response) { this.request = request; this.response = response; } get state() { return Config.getInstance().session.getState(this.sessionId); } get session() { return Config.getInstance().session; } // # pick data from session, eg. req.session // # AND // # return response based on the expected format of the ussd gateway }; // src/models/cache.model.ts var MENU_CACHE = {}; // src/models/ussd_state.model.ts var State = class _State { constructor() { this.pagination = {}; } get isStart() { return this.mode == "start" /* start */; } get isEnd() { return this.mode == "end" /* end */; } /** * Sets mode to "end" */ end() { this.mode = "end" /* end */; } static fromJSON(json) { return Object.assign(new _State(), json); } toJSON() { var _a; return { sessionId: this.sessionId, mode: this.mode, msisdn: this.msisdn, userData: this.userData, // nextMenu: this.nextMenu, menu: this.menu, action: this.action, previous: (_a = this.previous) == null ? void 0 : _a.toJSON(), // formInputId: this.formInputId, form: this.form, pagination: this.pagination }; } }; // src/gateways/wigal.gateway.ts var WigalGateway = class extends Gateway { get sessionId() { var _a; return (_a = this.request.query) == null ? void 0 : _a.sessionid; } async handleRequest() { var _a, _b, _c, _d, _e; let _state = await this.state; _state != null ? _state : _state = new State(); _state.mode = (_a = this.request.query) == null ? void 0 : _a.mode; _state.msisdn = (_b = this.request.query) == null ? void 0 : _b.msisdn; _state.sessionId = (_c = this.request.query) == null ? void 0 : _c.sessionid; _state.userData = (_d = this.request.query) == null ? void 0 : _d.userdata; this.request.state = _state; this.request.input = (_e = this.request.query) == null ? void 0 : _e.userdata; this.request.msisdn = _state.msisdn; return _state; } async handleResponse(_req, res) { res.writeHead(200, { "Content-Type": "text/plain" }); res.end(await this.wigalResponse()); return; } async wigalResponse() { var _a, _b, _c, _d, _e, _f; const data = await this.state; return `${(_a = this.request.query) == null ? void 0 : _a.network}|${data == null ? void 0 : data.mode}|${data == null ? void 0 : data.msisdn}|${data == null ? void 0 : data.sessionId}|${(_c = (_b = this.response.data) == null ? void 0 : _b.replace(/\n/g, "^")) != null ? _c : ""}|${(_d = this.request.query) == null ? void 0 : _d.username}|${(_e = this.request.query) == null ? void 0 : _e.trafficid}|${((_f = data == null ? void 0 : data.menu) == null ? void 0 : _f.nextMenu) || ""}`; } }; // src/sessions/base.session.ts var BaseSession = class { constructor() { this.states = {}; this.data = {}; } // TODO: change this to a proper configuration based on the session type async configure(options) { } }; // src/sessions/redis.session.ts import { createClient } from "redis"; var RedisSession = class _RedisSession extends BaseSession { constructor() { super(); } static getInstance() { if (!_RedisSession.instance) { _RedisSession.instance = new _RedisSession(); } return _RedisSession.instance; } async configure(options) { if (options == null) { throw new Error("Redis session configuration is required!"); } this.config = options; this.config.keyPrefix = (options == null ? void 0 : options.keyPrefix) || ""; await this.redisClient(); } async setState(sessionId, state) { this.states[sessionId] = state; await this.redisClient().then( (client) => client.set(`${sessionId}:state`, JSON.stringify(state.toJSON())) ); return state; } async getState(sessionId) { await this.redisClient(); const val = await this.CLIENT.get(`${sessionId}:state`); return val == null ? void 0 : State.fromJSON(JSON.parse(val)); } clear(sessionId) { const _state = this.states[sessionId]; delete this.states[sessionId]; delete this.data[sessionId]; this.redisClient().then((client) => client.del(`${sessionId}:*`)); return _state; } async set(sessionId, key, value) { await this.redisClient(); const val = await this.CLIENT.get(`${sessionId}:data`); const data = JSON.parse(val || "{}"); data[key] = value; await this.redisClient().then( (client) => client.set(`${sessionId}:data`, JSON.stringify(data)) ); } async remove(sessionId, key) { await this.redisClient(); const val = await this.CLIENT.get(`${sessionId}:data`); const data = JSON.parse(val || "{}"); delete data[key]; await this.redisClient().then( (client) => client.set(`${sessionId}:data`, JSON.stringify(data)) ); } async get(sessionId, key, defaultValue) { await this.redisClient(); const val = await this.CLIENT.get(`${sessionId}:data`); if (val == null) { return defaultValue; } return JSON.parse(val)[key] || defaultValue; } async getAll(sessionId) { const val = await this.redisClient().then( (client) => client.get(`${sessionId}:data`) ); if (val == null) { return void 0; } return JSON.parse(val); } async redisClient() { var _a; if (this.CLIENT == null) { if (this.config.url != null) { this.CLIENT = createClient({ url: this.config.url }); } else { this.CLIENT = createClient({ username: this.config.username, socket: { host: this.config.host || "localhost", port: this.config.port || 6379 }, database: this.config.database, password: this.config.password }); } } if (!((_a = this.CLIENT) == null ? void 0 : _a.isOpen)) { await this.CLIENT.connect(); } return this.CLIENT; } }; // src/sessions/postgresql.session.ts var PostgresSession = class _PostgresSession extends BaseSession { constructor() { super(); } static getInstance() { if (!_PostgresSession.instance) { _PostgresSession.instance = new _PostgresSession(); } return _PostgresSession.instance; } async configure(options) { var _a, _b, _c, _d, _e; if (options == null) { throw new Error("Postgres session configuration is required!"); } this.config = options; (_b = (_a = this.config).tableName) != null ? _b : _a.tableName = "ussd_sessions"; (_e = (_d = this.config).schema) != null ? _e : _d.schema = (_c = options == null ? void 0 : options.schema) != null ? _c : "public"; let pgPromise; try { pgPromise = await import("pg-promise"); } catch (error) { throw new Error( "'pg-promise' module is required for postgres session. Please install it using 'npm install pg-promise' or 'yarn add pg-promise'" ); } const pgp = pgPromise.default({ capSQL: true, // capitalize all generated SQL schema: [this.config.schema] }); this.db = pgp({ host: (options == null ? void 0 : options.host) || "localhost", port: (options == null ? void 0 : options.port) || 5432, database: options.database, user: options.username || "postgres", password: options.password }); } get softDeleteQuery() { if (this.config.softDelete == false || this.config.softDelete == null) return ""; return "AND deleted_at IS NULL"; } async setState(sessionId, state) { this.states[sessionId] = state; await this.db.none( `INSERT INTO $1~.$2~ (session_id, state, created_at, updated_at, deleted_at) VALUES ($3, $4::jsonb, $5, $5, NULL) ON CONFLICT (session_id) DO UPDATE SET state = $4::jsonb, updated_at = $5 WHERE $1~.$2~.session_id = $3 ${this.softDeleteQuery}`, [ this.config.schema, this.config.tableName, sessionId, JSON.stringify(state.toJSON()), (/* @__PURE__ */ new Date()).toISOString() ] ); return state; } async getState(sessionId) { const [val] = await this.db.any( `SELECT state FROM $1~.$2~ WHERE session_id = $3 ${this.softDeleteQuery} LIMIT 1`, [this.config.schema, this.config.tableName, sessionId] ); if (val == null) return void 0; if (typeof val === "object") return State.fromJSON(val.state); return State.fromJSON(JSON.parse(val.state)); } clear(sessionId) { const _state = this.states[sessionId]; delete this.states[sessionId]; delete this.data[sessionId]; if (this.config.softDelete === false || this.config.softDelete == null) { this.db.none("DELETE FROM $1~.$2~ WHERE session_id = $3", [ this.config.schema, this.config.tableName, sessionId ]).catch((error) => { throw error; }); } else { this.db.none( `UPDATE $1~.$2~ SET updated_at = $3, deleted_at = $3 WHERE session_id = $4 ${this.softDeleteQuery}`, [ this.config.schema, this.config.tableName, (/* @__PURE__ */ new Date()).toISOString(), sessionId ] ).catch((error) => { throw error; }); } return _state; } async set(sessionId, key, value) { const val = await this.db.one( `UPDATE $1~.$2~ SET data = jsonb_set(data, '{$3~}', $4::jsonb), updated_at = $5 WHERE session_id = $6 ${this.softDeleteQuery} RETURNING *`, [ this.config.schema, this.config.tableName, key, JSON.stringify(value), (/* @__PURE__ */ new Date()).toISOString(), sessionId ] ); return val; } async remove(sessionId, key) { const val = await this.db.one( `UPDATE $1~.$2~ SET data = data - '{$3}', updated_at = $4 WHERE session_id = $5 ${this.softDeleteQuery} RETURNING *`, [ this.config.schema, this.config.tableName, key, (/* @__PURE__ */ new Date()).toISOString(), sessionId ] ); return val; } async get(sessionId, key, defaultValue) { const [val] = await this.db.any( `SELECT data FROM $1~.$2~ WHERE session_id = $3 ${this.softDeleteQuery} LIMIT 1`, [this.config.schema, this.config.tableName, sessionId] ); if (val == null) { return defaultValue; } if (typeof val === "object") { return val[key] || defaultValue; } return JSON.parse(val)[key] || defaultValue; } async getAll(sessionId) { const [val] = await this.db.one( `SELECT data FROM $1~.$2~ WHERE session_id = $3 ${this.softDeleteQuery} LIMIT 1`, [this.config.schema, this.config.tableName, sessionId] ); if (val == null) { return void 0; } return JSON.parse(val); } }; // src/sessions/memcache.session.ts var MemcacheSession = class _MemcacheSession extends BaseSession { constructor() { super(); } static getInstance() { if (!_MemcacheSession.instance) { _MemcacheSession.instance = new _MemcacheSession(); } return _MemcacheSession.instance; } async setState(id, state) { this.states[id] = state; return state; } async getState(id) { return this.states[id]; } clear(id) { const _state = this.states[id]; delete this.states[id]; delete this.data[id]; return _state; } async set(sessionId, key, value) { if (this.data[sessionId] == null) { this.data[sessionId] = {}; } this.data[sessionId][key] = value; } async remove(sessionId, key) { var _a, _b; (_b = (_a = this.data)[sessionId]) != null ? _b : _a[sessionId] = {}; delete this.data[sessionId][key]; } async get(sessionId, key, defaultValue) { if (this.data[sessionId] == null) { return defaultValue; } return this.data[sessionId][key] || defaultValue; } async getAll(sessionId) { return this.data[sessionId]; } }; // src/sessions/mysql.session.ts var MySQLSession = class _MySQLSession extends BaseSession { constructor() { super(); } static getInstance() { if (!_MySQLSession.instance) { _MySQLSession.instance = new _MySQLSession(); } return _MySQLSession.instance; } async configure(options) { var _a, _b, _c; if (options == null) { throw new Error("Postgres session configuration is required!"); } this.config = options; (_b = (_a = this.config).tableName) != null ? _b : _a.tableName = "ussd_sessions"; let mysql; try { mysql = await import("mysql2/promise"); } catch (error) { throw new Error( "'mysql2/promise' module is required for postgres session. Please install it using 'npm install mysql2/promise' or 'yarn add mysql2/promise'" ); } this.db = await mysql.createConnection({ host: ((_c = this.config) == null ? void 0 : _c.host) || "localhost", user: this.config.username || "root", database: this.config.database, password: this.config.password }); } get softDeleteQuery() { if (this.config.softDelete == false || this.config.softDelete == null) return ""; return "AND deleted_at IS NULL"; } get tableName() { return this.config.tableName; } async setState(sessionId, state) { this.states[sessionId] = state; await this.db.query( `INSERT INTO ${this.tableName} (session_id, state, created_at, updated_at) VALUES (?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE state = ?`, [ sessionId, JSON.stringify(state.toJSON()), JSON.stringify(state.toJSON()), sessionId ] ); return state; } async getState(sessionId) { const [resp, _fields] = await this.db.query( `SELECT state, data FROM ${this.tableName} WHERE session_id = ? ${this.softDeleteQuery} LIMIT 1`, [sessionId] ); if (resp.length === 0) return void 0; const val = typeof resp[0] === "string" ? JSON.parse(resp[0]) : resp[0]; this.data[sessionId] = val.data; return State.fromJSON(val.state); } clear(sessionId) { const _state = this.states[sessionId]; delete this.states[sessionId]; delete this.data[sessionId]; if (this.config.softDelete === false || this.config.softDelete == null) { this.db.query(`DELETE FROM ${this.tableName} WHERE session_id = ?`, [ sessionId ]).catch((error) => { throw error; }); } else { this.db.query( `UPDATE ${this.tableName} SET deleted_at = ? WHERE session_id = ? ${this.softDeleteQuery}`, [(/* @__PURE__ */ new Date()).toISOString(), sessionId] ).catch((error) => { throw error; }); } return _state; } async set(sessionId, key, value) { var _a, _b; (_b = (_a = this.data)[sessionId]) != null ? _b : _a[sessionId] = {}; this.data[sessionId][key] = value; await this.db.query( `UPDATE ${this.tableName} SET data = ? WHERE session_id = ? ${this.softDeleteQuery}`, [JSON.stringify(this.data[sessionId]), sessionId] ); } async remove(sessionId, key) { var _a, _b; (_b = (_a = this.data)[sessionId]) != null ? _b : _a[sessionId] = {}; delete this.data[sessionId][key]; await this.db.query( `UPDATE ${this.tableName} SET data = ? WHERE session_id = ? ${this.softDeleteQuery}`, [JSON.stringify(this.data[sessionId]), sessionId] ); } async get(sessionId, key, defaultValue) { const [resp, _fields] = await this.db.query( `SELECT data FROM ${this.tableName} WHERE session_id = ? ${this.softDeleteQuery} LIMIT 1`, [sessionId] ); if (resp == null) { return defaultValue; } const val = typeof resp[0] === "string" ? JSON.parse(resp[0]) : resp[0]; return ((val == null ? void 0 : val.data) || "{}")[key] || defaultValue; } async getAll(sessionId) { const [[val]] = await this.db.query( `SELECT data FROM ${this.tableName} WHERE session_id = ? ${this.softDeleteQuery}`, [sessionId] ); if (val == null) { return void 0; } return JSON.parse(val.data || "{}"); } }; // src/types/config_options.type.ts var PaginationOption = class { constructor() { /** * Pagination is enabled by default if the content of the message to display is * more than the maximum 182 characters allowed. */ this.enabled = true; this.nextPage = { display: "*. More", choice: "*" }; this.previousPage = { display: "#. Back", choice: "#" }; } }; // src/gateways/emergent_technology.gateway.ts var EmergentTechnologyGateway = class extends Gateway { get sessionId() { var _a; return (_a = this.request.body) == null ? void 0 : _a.SessionId; } async handleRequest() { let _state = await this.state; _state != null ? _state : _state = new State(); const body = this.request.body; _state.mode = this.getMode(body.Type.toLowerCase()); _state.msisdn = body.Mobile; _state.sessionId = body.SessionId; _state.userData = body.Message; this.request.state = _state; this.request.msisdn = _state.msisdn; this.request.serviceCode = body.ServiceCode; if (_state.mode === "start" /* start */) { this.request.input = ""; } else { this.request.input = body.Message; } return _state; } async handleResponse(req, res) { res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ Message: this.response.data, Type: req.state.mode == "more" /* more */ ? "Response" : "Release" }) ); } getMode(type) { switch (type.toLowerCase()) { case "initiation": return "start" /* start */; case "response": return "more" /* more */; case "release": case "timeout": return "end" /* end */; default: return "start" /* start */; } } }; // src/config.ts var _gateway, _options; var _Config = class _Config { constructor() { __privateAdd(this, _gateway, void 0); __privateAdd(this, _options, void 0); this._session = void 0; } static getInstance() { if (!_Config.instance) { _Config.instance = new _Config(); } return _Config.instance; } init(options) { __privateSet(this, _options, options); if (typeof options.gateway == "string") { switch (options.gateway) { case "wigal" /* wigal */: __privateSet(this, _gateway, WigalGateway); break; case "emergent_technology" /* emergent_technology */: __privateSet(this, _gateway, EmergentTechnologyGateway); break; } } else { } const _session = options.session || "memory"; if (this._session instanceof BaseSession) { return this; } if (_session === "memory") { this._session = MemcacheSession.getInstance(); return this; } if (typeof _session === "object") { if ((_session == null ? void 0 : _session.type) != null) { switch (_session.type) { case "redis": this._session = RedisSession.getInstance(); this._session.configure(_session); break; case "postgres": this._session = PostgresSession.getInstance(); this._session.configure(_session); break; case "mssql": case "mysql": this._session = MySQLSession.getInstance(); this._session.configure(_session); break; default: throw new Error("Invalid session type"); } } } return this; } get gateway() { return __privateGet(this, _gateway); } get gatewayName() { return __privateGet(this, _options).gateway; } get session() { return this._session; } get options() { return __privateGet(this, _options); } }; _gateway = new WeakMap(); _options = new WeakMap(); var Config = _Config; // src/menus/action.menu.ts var MenuAction = class { // handler: (get: () => void, set: (val: any) => Promise<void>) => void = ( // get, // set // ) => { // return; // }; // get session(): Session { // return { // get: async <T>(key: string, defaultValue?: any) => { // return await Config.getInstance().session?.get<T>( // this.sessionId!, // key, // defaultValue // ); // }, // getAll: <T>() => { // return Config.getInstance().session?.getAll<T>(this.sessionId!); // }, // set: (key: string, val: any) => // Config.getInstance().session?.set(this.sessionId!, key, val), // }; // } }; // src/menus/base.menu.ts var BaseMenu = class { constructor(request, response) { this.request = request; this.response = response; } async validate(_data) { return true; } paginate() { return false; } /** * Terminate the current session * */ end() { return false; } get sessionId() { var _a; return (_a = this.request.query) == null ? void 0 : _a.sessionid; } isStart() { return false; } get session() { return { get: async (key, defaultValue) => { var _a; return await ((_a = Config.getInstance().session) == null ? void 0 : _a.get( this.sessionId, key, defaultValue )); }, getAll: () => { var _a; return (_a = Config.getInstance().session) == null ? void 0 : _a.getAll(this.sessionId); }, set: (key, val) => { var _a; return (_a = Config.getInstance().session) == null ? void 0 : _a.set(this.sessionId, key, val); }, remove: (key) => { var _a; return (_a = Config.getInstance().session) == null ? void 0 : _a.remove(this.sessionId, key); } }; } /** * Returns the current msisdn/phone number of the session. */ get msisdn() { return this.request.state.msisdn; } async back() { return void 0; } async inputs() { return []; } isForm() { return false; } }; // src/menus/dynamic_menu.menu.ts var _id, _formInputs, _isForm, _end, _paginate, _message, _nextMenu; var DynamicMenu = class { constructor(id, action) { // TODO: Look for better class name __privateAdd(this, _id, void 0); __privateAdd(this, _formInputs, []); __privateAdd(this, _isForm, false); __privateAdd(this, _end, false); __privateAdd(this, _paginate, false); __privateAdd(this, _message, void 0); __privateAdd(this, _nextMenu, void 0); this._isStart = false; this._currentOption = void 0; // make private?? this._action = void 0; __privateSet(this, _id, id); this._action = action; } isForm() { __privateSet(this, _isForm, true); return this; } defaultNextMenu(menu) { __privateSet(this, _nextMenu, menu); return this; } actions(items) { if (this._action !== void 0) { throw new Error( "Cannot set options for a menu with an action. Menu #${this._id} has an action defined" ); } this._actions = items; return this; } inputs(items) { var _a; (_a = __privateGet(this, _formInputs)) != null ? _a : __privateSet(this, _formInputs, []); __privateSet(this, _formInputs, [...__privateGet(this, _formInputs), ...items]); if (__privateGet(this, _formInputs).length === 0) { throw new Error(`Form menu #${this.id} must have at least one input!`); } return this; } start() { this._isStart = true; return this; } validation(val) { this._validation = val; return this; } message(msg) { __privateSet(this, _message, msg); return this; } paginate() { __privateSet(this, _paginate, true); return this; } /** * Terminate the current session */ end() { __privateSet(this, _end, true); } // TODO: rename to getactiona getActions() { return this._actions || []; } getInputs() { return __privateGet(this, _formInputs) || []; } async getMessage(req, res) { if (typeof __privateGet(this, _message) === "function") { return __privateGet(this, _message).call(this, req, res); } return __privateGet(this, _message); } async getDefaultNextMenu(req, res) { if (typeof __privateGet(this, _nextMenu) === "function") { return __privateGet(this, _nextMenu).call(this, req, res); } return __privateGet(this, _nextMenu); } async validateInput(req, res) { if (this._validation == null) { return true; } if (typeof this._validation === "function") { return this._validation(req, res); } try { return this._validation.test(req.state.userData); } catch (e) { } return false; } /** * Whether the current menu is a form menu or not. * Not to be confused with `isForm` which is used to set a menu as a form menu. * * **NOTE**: This is for internal use only! */ get isFormMenu() { return __privateGet(this, _isForm); } get action() { return this._action; } get id() { return __privateGet(this, _id); } get isEnd() { return __privateGet(this, _end); } get isStart() { return this._isStart || false; } set currentOption(value) { this._currentOption = value; } get currentOption() { return this._currentOption; } get isPaginated() { return __privateGet(this, _paginate); } }; _id = new WeakMap(); _formInputs = new WeakMap(); _isForm = new WeakMap(); _end = new WeakMap(); _paginate = new WeakMap(); _message = new WeakMap(); _nextMenu = new WeakMap(); // src/menus/index.ts var Menus = class _Menus { // private items: { [menuId: string]: Type<BaseMenu> | DynamicMenu } = {}; constructor() { } static getInstance() { if (!_Menus.instance) { _Menus.instance = new _Menus(); } return _Menus.instance; } add(cls, name) { MENU_CACHE[name] = { menu: cls, paginated: false }; } menu(id) { const _menu = new DynamicMenu(id); MENU_CACHE[id] = { menu: _menu, paginated: false }; return _menu; } get menus() { return MENU_CACHE; } async getStartMenu(req, res) { var _a, _b; let startId = null; for (const id in MENU_CACHE) { let isStart = false; const menu = (_a = MENU_CACHE[id]) == null ? void 0 : _a.menu; if (menuType(menu) === "class") { if (menu instanceof BaseMenu) { isStart = await menu.isStart(); } else { isStart = await new menu(req, res).isStart(); } } else { isStart = menu.isStart; } if (isStart) { startId = id; break; } } if (startId == null) { throw new Error("No start menu defined. Please define a start menu"); } return { id: startId, obj: (_b = MENU_CACHE[startId]) == null ? void 0 : _b.menu }; } getMenu(id) { var _a; const menu = (_a = MENU_CACHE[id]) == null ? void 0 : _a.menu; if (menu == void 0) { throw new Error(`Menu #${id} not found`); } return menu; } }; var MenuRouter = Menus.getInstance(); // src/helpers/menu.helper.ts function menuType(val) { if (/^DynamicMenu$/i.test(val.constructor.name)) { return "dynamic"; } return "class"; } async function getMenuActions(menu) { if (menuType(menu) === "class") { return await menu.actions() || []; } return await menu.getActions(); } // src/helpers/index.ts async function validateInput(opts) { const { state, menu, formInput: input, request, response } = opts; if (menu == null && input == null) { throw new Error("Either menu or input must be defined"); } let resp = { valid: true, error: void 0 }; let status = true; if (menu != null) { if (menuType(menu) == "class") { status = await menu.validate(state == null ? void 0 : state.userData); } else { status = await menu.validateInput(request, response); } } if (input != null) { if (input.validate == null) { status = true; } else if (typeof input.validate == "function") { status = await input.validate(request, response); } else { try { status = input.validate.test(state == null ? void 0 : state.userData); } catch (error) { } } } if (typeof status == "string") { resp = { valid: false, error: status }; } else if (typeof status == "boolean" && status == false) { resp = { valid: false, error: void 0 }; } return resp; } async function buildUserResponse(opts) { const { menu, state, errorMessage, response, request } = opts; if (errorMessage != null) { return errorMessage; } if (menu == null && state.isEnd) { return ""; } let message = void 0; if (menuType(menu) === "class") { message = await menu.message(); } else { message = await menu.getMessage(request, response); } if (message == null) { message = ""; } let actions = opts.actions; if (actions == null) { if (menuType(menu) === "class") { actions = await menu.actions() || []; } else { actions = await menu.getActions(); } } try { for (var iter = __forAwait(actions), more, temp, error; more = !(temp = await iter.next()).done; more = false) { const action = temp.value; if (action.display == null) continue; if (typeof action.display === "function") { message += ` ${await action.display(request, response)}`; } else { message += ` ${action.display}`; } } } catch (temp) { error = [temp]; } finally { try { more && (temp = iter.return) && await temp.call(iter); } finally { if (error) throw error[0]; } } return message; } // src/core/form_handler.ts var _currentInput, _nextInput, _formInputs2, _errorMessage; var FormMenuHandler = class { constructor(request, response, menu) { this.request = request; this.response = response; this.menu = menu; __privateAdd(this, _currentInput, void 0); __privateAdd(this, _nextInput, void 0); __privateAdd(this, _formInputs2, []); __privateAdd(this, _errorMessage, void 0); } get submittedInputs() { var _a, _b; return Object.keys((_b = (_a = this.state.form) == null ? void 0 : _a.submitted) != null ? _b : {}) || []; } get state() { return this.request.state; } get session() { return Config.getInstance().session; } async endSession() { this.state.end(); await this.session.clear(this.state.sessionId); } // TODO: extract menu type to a separate type. is `any` for testing now async handle() { var _a, _b, _c, _d, _e, _f, _g; (_c = (_b = this.request.state).form) != null ? _c : _b.form = { id: (_a = this.state.menu) == null ? void 0 : _a.nextMenu, nextInput: void 0, submitted: {} }; if (menuType(this.menu) == "class") { __privateSet(this, _formInputs2, await this.menu.inputs()); } else { __privateSet(this, _formInputs2, this.menu.getInputs()); } if (this.submittedInputs.length == 0 && ((_d = this.state.form) == null ? void 0 : _d.nextInput) == null) { __privateSet(this, _currentInput, __privateGet(this, _formInputs2)[0]); this.state.form.nextInput = __privateGet(this, _currentInput).name; this.response.data = await this.buildResponse(__privateGet(this, _currentInput)); return __privateGet(this, _errorMessage) != null ? void 0 : (_e = __privateGet(this, _currentInput)) == null ? void 0 : _e.next_menu; } if (((_f = this.state.form) == null ? void 0 : _f.nextInput) != null) { __privateSet(this, _currentInput, __privateGet(this, _formInputs2).find( (item) => { var _a2; return item.name == ((_a2 = this.state.form) == null ? void 0 : _a2.nextInput); } )); } await this.handleInput(); this.response.data = await this.buildResponse(__privateGet(this, _nextInput)); return __privateGet(this, _errorMessage) != null ? void 0 : (_g = __privateGet(this, _currentInput)) == null ? void 0 : _g.next_menu; } async handleInput() { var _a, _b, _c; if (__privateGet(this, _currentInput) == null) { throw new Error( `Input #${(_a = this.state.form) == null ? void 0 : _a.id} is not defined in form/menu #${this.state.menu}` ); } if (__privateGet(this, _currentInput).validate != null) { let result = await validateInput({ state: this.state, formInput: __privateGet(this, _currentInput), request: this.request, response: this.response }); if (!result.valid) { __privateSet(this, _errorMessage, result.error || await this.getDisplayText(__privateGet(this, _currentInput))); return; } const temp = (_b = await this.session.get( this.state.sessionId, this.state.form.id )) != null ? _b : {}; temp[__privateGet(this, _currentInput).name] = this.state.userData; await this.session.set(this.state.sessionId, (_c = this.state.form) == null ? void 0 : _c.id, temp); } if (__privateGet(this, _currentInput).handler != null) { await __privateGet(this, _currentInput).handler(this.request); } this.state.form.submitted[__privateGet(this, _currentInput).name] = true; if (__privateGet(this, _currentInput).end != null) { let end = typeof __privateGet(this, _currentInput).end == "function" ? await __privateGet(this, _currentInput).end(this.request) : __privateGet(this, _currentInput).end; if (end && __privateGet(this, _currentInput).next_menu == null) { return await this.endSession(); } } await this.resolveNextInput(); } async resolveNextInput() { var _a, _b, _c, _d, _e; if (((_a = __privateGet(this, _currentInput)) == null ? void 0 : _a.next_input) == null && ((_b = __privateGet(this, _currentInput)) == null ? void 0 : _b.next_menu) == null) { return await this.endSession(); } if (((_c = __privateGet(this, _currentInput)) == null ? void 0 : _c.next_input) == null && ((_d = __privateGet(this, _currentInput)) == null ? void 0 : _d.next_menu) != null) { return; } if (typeof __privateGet(this, _currentInput).next_input == "string") { this.state.form.nextInput = __privateGet(this, _currentInput).next_input; } else if (typeof __privateGet(this, _currentInput).next_input == "function") { this.state.form.nextInput = await __privateGet(this, _currentInput).next_input( this.request ); } __privateSet(this, _nextInput, await __privateGet(this, _formInputs2).find( (item) => { var _a2; return item.name == ((_a2 = this.state.form) == null ? void 0 : _a2.nextInput); } )); if (__privateGet(this, _nextInput) == null) { await this.endSession(); throw new Error( `Input #${(_e = this.state.form) == null ? void 0 : _e.nextInput} is not defined in form/menu #${this.state.menu}` ); } } async buildResponse(input) { let message = ""; if (__privateGet(this, _errorMessage) != null) { return __privateGet(this, _errorMessage); } return await this.getDisplayText(input); } async getDisplayText(input) { if (typeof (input == null ? void 0 : input.display) == "function") { return await input.display(this.request); } return input == null ? void 0 : input.display; } }; _currentInput = new WeakMap(); _nextInput = new WeakMap(); _formInputs2 = new WeakMap(); _errorMessage = new WeakMap(); // src/core/pagination_handler.ts var PaginationHandler = class _PaginationHandler { constructor(request, response, menu, menuId) { this.request = request; this.response = response; this.menu = menu; this.menuId = menuId; } get state() { return this.request.state; } async handle() { var _a, _b; const paginationState = this.state.pagination == null ? null : this.state.pagination[this.menuId]; if (paginationState != null) { return; } let actions = await getMenuActions(this.menu); const pages = await this.generatePaginationItems({ actions, // item: undefined, menu: this.menu, unpaginatedActions: [], page: 1, pages: [] }); if (pages != null) { (_b = (_a = this.state).pagination) != null ? _b : _a.pagination = {}; this.state.pagination[this.menuId] = { currentPage: void 0, pages }; } } async generatePaginationItems(opts) { const isStart = opts.page == 1; const { actions } = opts; let message = await buildUserResponse({ state: this.state, errorMessage: void 0, request: this.request, response: this.response, menu: opts.menu, actions }); if (actions.length === 0) { return opts.pages; } message += this.buildNavigationAction(isStart); const charactersCount = message.length; if (charactersCount > MAXIMUM_CHARACTERS) { const temp2 = [...opts.actions]; opts.unpaginatedActions.push(temp2.pop()); return this.generatePaginationItems({ menu: opts.menu, actions: temp2, unpaginatedActions: opts.unpaginatedActions, // item: opts.item, page: opts.page, pages: opts.pages }); } const conf = _PaginationHandler.paginationConfig; if (!isStart) { actions.push({ choice: conf.previousPage.choice, display: conf.previousPage.display, next_menu: this.menuId }); } if (opts.unpaginatedActions.length > 0) { actions.push({ choice: conf.nextPage.choice, display: conf.nextPage.display, next_menu: this.menuId }); } let paginationItem = { page: opts.page, nextPage: void 0, previousPage: !isStart ? opts.page - 1 : void 0, data: [...actions] }; if (opts.unpaginatedActions.length > 0) { opts.page += 1; paginationItem.nextPage = opts.page; } opts.pages.push(paginationItem); const temp = [...opts.unpaginatedActions]; opts.unpaginatedActions = []; return this.generatePaginationItems({ menu: opts.menu, actions: temp, unpaginatedActions: opts.unpaginatedActions, page: opts.page, pages: opts.pages }); } buildNavigationAction(isStart = false) { const conf = _PaginationHandler.paginationConfig; if (isStart) { return "\n" + conf.nextPage.display; } return ` ${conf.previousPage.display} ${conf.nextPage.display}`; } static get paginationConfig() { var _a; return (_a = Config.getInstance().options.pagination) != null ? _a : new PaginationOption(); } static isNavActionSelected(input) { var _a, _b, _c, _d; if (input == null) { return false; } const options = [ (_b = (_a = this.paginationConfig.previousPage) == null ? void 0 : _a.choice) == null ? void 0 : _b.trim(), (_d = (_c = this.paginationConfig.nextPage) == null ? void 0 : _c.choice) == null ? void 0 : _d.trim() ]; return options.includes(input == null ? void 0 : input.trim()); } static isPageActionSelected(state) { var _a; return !this.isNavActionSelected((_a = state.userData) == null ? void 0 : _a.trim()); } static shouldGoToPreviousPage(input) { var _a, _b; if (input == null) { return false; } return input.trim() == ((_b = (_a = this.paginationConfig.previousPage) == null ? void 0 : _a.choice) == null ? void 0 : _b.trim()); } static navigateToPage(state) { var _a, _b, _c; const input = (_a = state.userData) == null ? void 0 : _a.trim(); let { pages, currentPage } = state.pagination[(_b = state.menu) == null ? void 0 : _b.nextMenu]; if (currentPage == null) { currentPage = pages.find((i) => i.page == 1); } else if (_PaginationHandler.shouldGoToPreviousPage(input)) { currentPage = pages.find((i) => i.page == (currentPage == null ? void 0 : currentPage.previousPage)); if (currentPage == null) { currentPage = pages.find((i) => i.page == 1); } } else { currentPage = pages.find((i) => i.page == (currentPage == null ? void 0 : currentPage.nextPage)); } state.pagination[(_c = state.menu) == null ? void 0 : _c.nextMenu].currentPage = currentPage; return (currentPage == null ? void 0 : currentPage.data) || []; } }; // src/core/request_handler.ts var RequestHandler = class { constructor(request, response, router) { this.request = request; this.response = response; this.router = router; } async setCurrentMenu(id, state) { var _a; (_a = state.menu) != null ? _a : state.menu = { nextMenu: void 0, visited: {} }; state.menu.nextMenu = id; await this.session.setState(state.sessionId, state); } get config() { return Config.getInstance(); } get session() { return this.config.session; } async isFormMenu(menu) { if (menuType(menu) == "class") { return await menu.isForm() || false; } else { return await menu.isFormMenu || false; } } async isPaginatedMenu(menu) { if (menuType(menu) == "class") { return await menu.paginate() || false; } else { return await menu.isPaginated || false; } } async formHandler(currentMenu, state) { const formHandler = new FormMenuHandler( this.request, this.response, currentMenu ); const nextMenu = await formHandler.handle(); if (!state.isEnd) { await this.session.setState(state.sessionId, state); } if (nextMenu != null) { currentMenu = (await this.navigateToNextMenu(state, nextMenu)).menu; this.response.data = await this.buildResponse(currentMenu, state); await this.resolveGateway("response"); return this.response; } await this.resolveGateway("response"); return this.response; } async processRequest() { var _a, _b, _c, _d; this.router = MenuRouter; let currentMenu = void 0; const state = await this.resolveGateway("request"); (_a = state.menu) != null ? _a : state.menu = { nextMenu: void 0, visited: {} }; this.request.state = state; this.request.session = this.getSession(); if (((_b = state.menu) == null ? void 0 : _b.nextMenu) == null) { currentMenu = await this.lookupMenu(state); this.response.data = await this.buildResponse(currentMenu, state); await this.resolveGateway("response"); return this.response; } else { currentMenu = await this.lookupMenu(state); } if (await this.isFormMenu(currentMenu)) { return this.formHandler(currentMenu, state); } if (await this.isPaginatedMenu(currentMenu)) { if (PaginationHandler.isPageActionSelected(state)) { await this.lookupMenuOptions(state, currentMenu); } else { await new PaginationHandler( this.request, this.response, currentMenu, (_c = state.menu) == null ? void 0 : _c.nextMenu ).handle(); } } else { await this.lookupMenuOptions(state, currentMenu); } const error = await this.validateUserData(state, currentMenu); if (error != null) { this.response.data = error; await this.resolveGateway("response"); return this.response; } let nextMenu = await this.resolveNextMenu( state, currentMenu ); if (nextMenu != null) { if (await this.isPaginatedMenu(nextMenu)) { await new PaginationHandler( this.request, this.response, nextMenu, (_d = state.menu) == null ? void 0 : _d.nextMenu ).handle(); } else { if (await this.isFormMenu(nextMenu)) { return this.formHandler(nextMenu, state); } let isEnd = false; if (menuType(nextMenu) == "class") { isEnd = await nextMenu.end(); } else { isEnd = nextMenu.isEnd; } if (isEnd) { state.end(); } } } this.response.data = await this.buildResponse(nextMenu, state); await this.resolveGateway("response"); return this.response; } async validateUserData(state, menu, error) { var _a; if (await this.isPaginatedMenu(menu) && PaginationHandler.isNavActionSelected((_a = state.userData) == null ? void 0 : _a.trim())) { return void 0; } let result = await validateInput({ state, menu, request: this.request, response: this.response }); if (!result.valid) { error = result.error || await this.buildResponse(menu, state); return error; } return error; } async buildResponse(menu, state, errorMessage) { if (menu != null && await this.isPaginatedMenu(menu)) { return buildUserResponse({ menu, actions: PaginationHandler.navigateToPage(state), state, errorMessage, request: this.request, response: this.response }); } return buildUserResponse({ menu, state, errorMessage, request: this.request, response: this.response }); } async resolveNextMenu(state, currentMenu) { var _a; if (await this.isPaginatedMenu(currentMenu)) { if (PaginationHandler.isNavActionSelected((_a = state.userData) == null ? void 0 : _a.trim())) { return currentMenu; } } const action = state.action; if ((action == null ? void 0 : action.handler) != null) { await action.handler(this.request); } const resp = await this.navigateToNextMenu(state, action == null ? void 0 : action.next_menu); if (resp.status) { return resp.menu; } if (menuType(currentMenu) == "class") { const _next2 = await currentMenu.nextMenu(); if (_next2 == null) { await this.endSession(state); return void 0; } return (await this.navigateToNextMenu(state, _next2)).menu; } const _next = await currentMenu.getDefaultNextMenu( this.request, this.response ); if (_next != null) { return (await this.navigateToNextMenu(state, _next)).menu; } await this.endSession(state); return void 0; } /** * Resolve next menu and make it the current menu. */ async navigateToNextMenu(state, next_menu) { let status = false, menu = void 0, id = void 0; if (next_menu == null) return { status, menu }; if (typeof next_menu == "string") { id = next_menu; status = true; } else if (typeof next_menu == "function") { id = await next_menu(this.request, this.response); status = true; } if (status) { menu = this.instantiateMenu(this.router.getMenu(id)); state.menu.visited[state.menu.nextMenu] = true; delete state.menu.visited[id]; this.setCurrentMenu(id, state); } return { status, menu }; } instantiateMenu(menu) { if (menuType(menu) == "class") { if (menu instanceof BaseMenu) { return menu; } return new menu(this.request, this.response); } return menu; } async lookupMenuOptions(state, menu) { const actions = await getMenuActions(menu); const input = state.userData;