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
JavaScript
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;