841 lines
24 KiB
JavaScript
841 lines
24 KiB
JavaScript
const fs = require('fs-extra');
|
|
const Scope = require('./scope');
|
|
const Parser = require('./parser');
|
|
const db = require('./db');
|
|
const validator = require('../validator');
|
|
const config = require('../setup/config');
|
|
const debug = require('debug')('server-connect:app');
|
|
const debug2 = require('debug')('server-connect:output');
|
|
const { clone, formatDate, parseDate } = require('../core/util');
|
|
const { toSystemPath } = require('../core/path');
|
|
const os = require('os');
|
|
|
|
if (!global.fileCache) {
|
|
global.fileCache = new Map();
|
|
}
|
|
|
|
if (!global.db) {
|
|
global.db = {};
|
|
}
|
|
|
|
if (!global.rateLimiter) {
|
|
global.rateLimiter = {};
|
|
}
|
|
|
|
function App(req = {}, res = {}) {
|
|
this.error = false;
|
|
this.data = {};
|
|
this.meta = {};
|
|
this.settings = {};
|
|
this.modules = {};
|
|
|
|
this.io = global.io;
|
|
this.req = req;
|
|
this.res = res;
|
|
this.global = new Scope();
|
|
this.scope = this.global;
|
|
|
|
this.rateLimiter = {};
|
|
this.mail = {};
|
|
this.auth = {};
|
|
this.oauth = {};
|
|
this.db = {};
|
|
this.s3 = {};
|
|
this.jwt = {};
|
|
this.trx = {};
|
|
|
|
if (config.globals.data) {
|
|
this.set(config.globals.data);
|
|
}
|
|
|
|
this.set({
|
|
$_ERROR: null,
|
|
//$_SERVER: process.env,
|
|
$_ENV: process.env,
|
|
$_GET: req.query,
|
|
$_POST: req.body,
|
|
$_PARAM: req.params,
|
|
$_HEADER: req.headers,
|
|
$_COOKIE: req.cookies,
|
|
$_SESSION: req.session,
|
|
});
|
|
|
|
let urlParts = req.originalUrl ? req.originalUrl.split('?') : ['', ''];
|
|
let $server = {
|
|
CONTENT_TYPE: req.headers && req.headers['content-type'],
|
|
HTTPS: req.protocol == 'https',
|
|
PATH_INFO: req.path,
|
|
QUERY_STRING: urlParts[1],
|
|
REMOTE_ADDR: req.ip,
|
|
REQUEST_PROTOCOL: req.protocol,
|
|
REQUEST_METHOD: req.method,
|
|
SERVER_NAME: req.hostname,
|
|
BASE_URL: req.headers && req.protocol + '://' + req.headers['host'],
|
|
URL: urlParts[0],
|
|
HOSTNAME: os.hostname(),
|
|
};
|
|
|
|
if (req.headers) {
|
|
for (let header in req.headers) {
|
|
$server['HTTP_' + header.toUpperCase().replace(/-/, '_')] = req.headers[header];
|
|
}
|
|
}
|
|
|
|
// Try to keep same as ASP/PHP
|
|
this.set('$_SERVER', $server);
|
|
}
|
|
|
|
App.prototype = {
|
|
set: function (key, value) {
|
|
this.global.set(key, value);
|
|
},
|
|
|
|
get: function (key, def) {
|
|
let value = this.global.get(key);
|
|
return value !== undefined ? value : def;
|
|
},
|
|
|
|
remove: function (key) {
|
|
this.global.remove(key);
|
|
},
|
|
|
|
setSession: function (key, value) {
|
|
this.req.session[key] = value;
|
|
},
|
|
|
|
getSession: function (key) {
|
|
return this.req.session[key];
|
|
},
|
|
|
|
removeSession: function (key) {
|
|
delete this.req.session[key];
|
|
},
|
|
|
|
regenerateSessionId: function () {
|
|
return new Promise((resolve, reject) => {
|
|
const oldSession = this.req.session;
|
|
this.req.session.regenerate((err) => {
|
|
if (err) return reject(err);
|
|
|
|
for (let key in oldSession) {
|
|
this.req.session[key] = oldSession[key];
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
},
|
|
|
|
setCookie: function (name, value, opts) {
|
|
if (!this.res.headersSent) {
|
|
if (this.res.cookie) {
|
|
this.res.cookie(name, value, opts);
|
|
}
|
|
} else {
|
|
debug(`Trying to set ${name} cookie while headers were already sent.`);
|
|
}
|
|
},
|
|
|
|
getCookie: function (name, signed) {
|
|
return signed ? this.req.signedCookies[name] : this.req.cookies[name];
|
|
},
|
|
|
|
removeCookie: function (name, opts) {
|
|
if (this.res.clearCookie) {
|
|
// copy all options except expires and maxAge
|
|
const clearOpts = Object.assign({}, opts);
|
|
delete clearOpts.expires;
|
|
delete clearOpts.maxAge;
|
|
this.res.clearCookie(name, clearOpts);
|
|
}
|
|
},
|
|
|
|
setRateLimiter: function (name, options) {
|
|
const { RateLimiterMemory, RateLimiterRedis } = require('rate-limiter-flexible');
|
|
|
|
if (global.redisClient) {
|
|
global.rateLimiter[name] = new RateLimiterRedis({
|
|
...options,
|
|
storeClient: global.redisClient,
|
|
keyPrefix: 'ac:' + name + ':',
|
|
});
|
|
} else {
|
|
global.rateLimiter[name] = new RateLimiterMemory({
|
|
...options,
|
|
keyPrefix: 'ac:' + name + ':',
|
|
});
|
|
}
|
|
|
|
return global.rateLimiter[name];
|
|
},
|
|
|
|
getRateLimiter: function (name) {
|
|
if (global.rateLimiter[name]) {
|
|
return global.rateLimiter[name];
|
|
}
|
|
|
|
if (config.rateLimiter[name]) {
|
|
return this.setRateLimiter(name, config.rateLimiter[name]);
|
|
}
|
|
|
|
if (fs.existsSync(`app/modules/RateLimiter/${name}.json`)) {
|
|
let action = fs.readJSONSync(`app/modules/RateLimiter/${name}.json`);
|
|
return this.setRateLimiter(name, action.options);
|
|
}
|
|
|
|
throw new Error(`Couldn't find rate limiter "${name}".`);
|
|
},
|
|
|
|
setMailer: function (name, options) {
|
|
let setup = {};
|
|
|
|
options = this.parse(options);
|
|
|
|
switch (options.server) {
|
|
case 'mail':
|
|
setup.sendmail = true;
|
|
break;
|
|
|
|
case 'ses':
|
|
const aws = require('@aws-sdk/client-ses');
|
|
const ses = new AWS.SES({
|
|
endpoint: options.endpoint,
|
|
credentials: {
|
|
accessKeyId: options.accessKeyId,
|
|
secretAccessKey: options.secretAccessKey,
|
|
},
|
|
});
|
|
setup.SES = { ses, aws };
|
|
break;
|
|
|
|
default:
|
|
// https://nodemailer.com/smtp/
|
|
setup.host = options.host || 'localhost';
|
|
setup.port = options.port || 25;
|
|
setup.secure = options.useSSL || false;
|
|
setup.auth = {
|
|
user: options.username,
|
|
pass: options.password,
|
|
};
|
|
setup.tls = {
|
|
rejectUnauthorized: false,
|
|
};
|
|
}
|
|
|
|
return (this.mail[name] = setup);
|
|
},
|
|
|
|
getMailer: function (name) {
|
|
if (this.mail[name]) {
|
|
return this.mail[name];
|
|
}
|
|
|
|
if (config.mail[name]) {
|
|
return this.setMailer(name, config.mail[name]);
|
|
}
|
|
|
|
if (fs.existsSync(`app/modules/Mailer/${name}.json`)) {
|
|
let action = fs.readJSONSync(`app/modules/Mailer/${name}.json`);
|
|
return this.setMailer(name, action.options);
|
|
}
|
|
|
|
throw new Error(`Couldn't find mailer "${name}".`);
|
|
},
|
|
|
|
setAuthProvider: async function (name, options) {
|
|
const Provider = require('../auth/' + options.provider.toLowerCase());
|
|
const provider = new Provider(this, options, name);
|
|
if (!provider.identity) await provider.autoLogin();
|
|
this.set('identity', provider.identity);
|
|
return (this.auth[name] = provider);
|
|
},
|
|
|
|
getAuthProvider: async function (name) {
|
|
if (this.auth[name]) {
|
|
return this.auth[name];
|
|
}
|
|
|
|
if (config.auth[name]) {
|
|
return await this.setAuthProvider(name, config.auth[name]);
|
|
}
|
|
|
|
if (fs.existsSync(`app/modules/SecurityProviders/${name}.json`)) {
|
|
let action = fs.readJSONSync(`app/modules/SecurityProviders/${name}.json`);
|
|
return await this.setAuthProvider(name, action.options);
|
|
}
|
|
|
|
throw new Error(`Couldn't find security provider "${name}".`);
|
|
},
|
|
|
|
setOAuthProvider: async function (name, options) {
|
|
const OAuth2 = require('../oauth');
|
|
const services = require('../oauth/services');
|
|
|
|
options = this.parse(options);
|
|
|
|
let service = this.parseOptional(options.service, 'string', null);
|
|
let opts = service ? services[service] : {};
|
|
opts.client_id = this.parseOptional(options.client_id, 'string', null);
|
|
opts.client_secret = this.parseOptional(options.client_secret, 'string', null);
|
|
opts.token_endpoint =
|
|
opts.token_endpoint ||
|
|
this.parseRequired(options.token_endpoint, 'string', 'oauth.provider: token_endpoint is required.');
|
|
opts.auth_endpoint = opts.auth_endpoint || this.parseOptional(options.auth_endpoint, 'string', '');
|
|
opts.scope_separator = opts.scope_separator || this.parseOptional(options.scope_separator, 'string', ' ');
|
|
opts.access_token = this.parseOptional(options.access_token, 'string', null);
|
|
opts.refresh_token = this.parseOptional(options.refresh_token, 'string', null);
|
|
opts.jwt_bearer = this.parseOptional(options.jwt_bearer, 'string', false);
|
|
opts.client_credentials = this.parseOptional(options.client_credentials, 'boolean', false);
|
|
opts.params = Object.assign({}, opts.params, this.parseOptional(options.params, 'object', {}));
|
|
|
|
this.oauth[name] = new OAuth2(this, this.parse(options), name);
|
|
await this.oauth[name].init();
|
|
|
|
return this.oauth[name];
|
|
},
|
|
|
|
getOAuthProvider: async function (name) {
|
|
if (this.oauth[name]) {
|
|
return this.oauth[name];
|
|
}
|
|
|
|
if (config.oauth[name]) {
|
|
return await this.setOAuthProvider(name, config.oauth[name]);
|
|
}
|
|
|
|
if (fs.existsSync(`app/modules/oauth/${name}.json`)) {
|
|
let action = fs.readJSONSync(`app/modules/oauth/${name}.json`);
|
|
return await this.setOAuthProvider(name, action.options);
|
|
}
|
|
|
|
throw new Error(`Couldn't find oauth provider "${name}".`);
|
|
},
|
|
|
|
setDbConnection: function (name, options) {
|
|
if (global.db[name]) {
|
|
return global.db[name];
|
|
}
|
|
|
|
options = this.parse(options);
|
|
|
|
switch (options.client) {
|
|
case 'couchdb':
|
|
const { host, port, user, password, database } = options.connection;
|
|
const nano = require('nano');
|
|
global.db[name] = nano(
|
|
`http://${user ? user + (password ? ':' + password : '') + '@' : ''}${host}${
|
|
port ? ':' + port : ''
|
|
}/${database}`
|
|
);
|
|
global.db[name].client = options.client;
|
|
return global.db[name];
|
|
|
|
case 'sqlite3':
|
|
if (options.connection.filename) {
|
|
options.connection.filename = toSystemPath(options.connection.filename);
|
|
}
|
|
break;
|
|
|
|
case 'mysql':
|
|
case 'mysql2':
|
|
options.connection = {
|
|
supportBigNumbers: true,
|
|
dateStrings: !!options.tz,
|
|
decimalNumbers: true,
|
|
...options.connection,
|
|
};
|
|
/*
|
|
options.connection.typeCast = function(field, next) {
|
|
if (field.type == 'NEWDECIMAL') {
|
|
return field.string() + 'm';
|
|
}
|
|
|
|
return next();
|
|
};
|
|
*/
|
|
break;
|
|
|
|
case 'mssql':
|
|
if (options.tz) {
|
|
// use local timezone to have same behavior as the other drivers
|
|
// to prevent problems node should have same timezone as database
|
|
options.connection.options = { useUTC: false, ...options.connection.options };
|
|
}
|
|
break;
|
|
|
|
case 'postgres':
|
|
if (options.tz) {
|
|
const types = require('pg').types;
|
|
const parseFn = (val) => val;
|
|
types.setTypeParser(types.builtins.TIME, parseFn);
|
|
types.setTypeParser(types.builtins.TIMETZ, parseFn);
|
|
types.setTypeParser(types.builtins.TIMESTAMP, parseFn);
|
|
types.setTypeParser(types.builtins.TIMESTAMPTZ, parseFn);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (options.connection && options.connection.ssl) {
|
|
if (options.connection.ssl.key) {
|
|
options.connection.ssl.key = fs.readFileSync(toSystemPath(options.connection.ssl.key));
|
|
}
|
|
|
|
if (options.connection.ssl.ca) {
|
|
options.connection.ssl.ca = fs.readFileSync(toSystemPath(options.connection.ssl.ca));
|
|
}
|
|
|
|
if (options.connection.ssl.cert) {
|
|
options.connection.ssl.cert = fs.readFileSync(toSystemPath(options.connection.ssl.cert));
|
|
}
|
|
}
|
|
|
|
options.useNullAsDefault = true;
|
|
|
|
const formatRecord = (record, meta) => {
|
|
for (column in record) {
|
|
if (record[column] != null) {
|
|
if (meta.has(column)) {
|
|
const info = meta.get(column);
|
|
|
|
if (['json', 'object', 'array'].includes(info.type)) {
|
|
if (typeof record[column] == 'string') {
|
|
try {
|
|
// column of type json returned as string, need parse
|
|
record[column] = JSON.parse(record[column]);
|
|
} catch (err) {
|
|
console.warn(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (info.type == 'date') {
|
|
if (typeof record[column] == 'string' && record[column].startsWith('0000-00-00')) {
|
|
record[column] = undefined;
|
|
} else {
|
|
record[column] = formatDate(record[column], 'yyyy-MM-dd');
|
|
}
|
|
}
|
|
|
|
if (info.type == 'time') {
|
|
if (options.tz == 'local') {
|
|
record[column] = formatDate(record[column], 'HH:mm:ss.v');
|
|
} else if (options.tz == 'utc') {
|
|
record[column] = parseDate(
|
|
parseDate(formatDate('now', 'yyyy-MM-dd ') + formatDate(record[column], 'HH:mm:ss.v'))
|
|
)
|
|
.toISOString()
|
|
.slice(11);
|
|
}
|
|
}
|
|
|
|
if (
|
|
['datetime', 'timestamp'].includes(info.type) ||
|
|
Object.prototype.toString.call(record[column]) == '[object Date]'
|
|
) {
|
|
if (typeof record[column] == 'string' && record[column].startsWith('0000-00-00')) {
|
|
record[column] = undefined;
|
|
} else if (options.tz == 'local') {
|
|
record[column] = formatDate(record[column], 'yyyy-MM-dd HH:mm:ss.v');
|
|
} else if (options.tz == 'utc') {
|
|
record[column] = parseDate(record[column]).toISOString();
|
|
}
|
|
}
|
|
} else {
|
|
// try to detect datetime
|
|
if (Object.prototype.toString.call(record[column]) == '[object Date]') {
|
|
if (options.tz == 'local') {
|
|
record[column] = formatDate(record[column], 'yyyy-MM-dd HH:mm:ss.v');
|
|
} else if (options.tz == 'utc') {
|
|
record[column] = parseDate(record[column]).toISOString();
|
|
}
|
|
} else if (
|
|
typeof record[column] == 'string' &&
|
|
/^\d{4}-\d{2}-\d{2}([T\s]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}(:\d{2})?)?)?$/.test(record[column])
|
|
) {
|
|
if (record[column].startsWith('0000-00-00')) {
|
|
record[column] = undefined;
|
|
} else if (options.tz == 'local') {
|
|
record[column] = formatDate(record[column], 'yyyy-MM-dd HH:mm:ss.v');
|
|
} else if (options.tz == 'utc') {
|
|
record[column] = parseDate(record[column]).toISOString();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (record[column] == undefined) {
|
|
record[column] = null;
|
|
} else if (record[column].toJSON) {
|
|
record[column] = record[column].toJSON();
|
|
}
|
|
}
|
|
}
|
|
|
|
return record;
|
|
};
|
|
|
|
options.postProcessResponse = function (result, queryContext) {
|
|
const meta = new Map();
|
|
|
|
if (Array.isArray(queryContext)) {
|
|
for (let item of queryContext) {
|
|
if (item.name && item.type) {
|
|
meta.set(item.name, item);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(result)) {
|
|
return result.map((record) => formatRecord(record, meta));
|
|
} else {
|
|
return formatRecord(result, meta);
|
|
}
|
|
};
|
|
|
|
global.db[name] = db(options);
|
|
|
|
return global.db[name];
|
|
},
|
|
|
|
getDbConnection: function (name) {
|
|
if (config.db[name]) {
|
|
return this.setDbConnection(name, config.db[name]);
|
|
}
|
|
|
|
const path = `app/modules/connections/${name}.json`;
|
|
const action = global.fileCache.get(path) || fs.readJSONSync(`app/modules/connections/${name}.json`);
|
|
const isDynamic = this.isDynamic(action);
|
|
const options = clone(action.options); // need to clone because this.parse will update object by ref
|
|
|
|
if (!global.fileCache.has(path)) {
|
|
// if action was not already cached do it now
|
|
global.fileCache.set(path, action);
|
|
}
|
|
|
|
if (isDynamic) {
|
|
// since it is only used internal json stringify is good enough for creating a unique name
|
|
name = JSON.stringify(this.parse(options));
|
|
}
|
|
|
|
if (this.trx[name]) return this.trx[name];
|
|
|
|
return this.setDbConnection(name, options);
|
|
},
|
|
|
|
setS3Provider: function (name, options) {
|
|
options = this.parse(options);
|
|
|
|
const { S3 } = require('@aws-sdk/client-s3');
|
|
const endpoint = this.parseRequired(options.endpoint, 'string', 's3.provider: endpoint is required.');
|
|
const accessKeyId = this.parseRequired(options.accessKeyId, 'string', 's3.provider: accessKeyId is required.');
|
|
const secretAccessKey = this.parseRequired(
|
|
options.secretAccessKey,
|
|
'string',
|
|
's3.provider: secretAccessKey is required.'
|
|
);
|
|
|
|
let region = options.region || 'us-east-1';
|
|
let pos = endpoint.indexOf('.amazonaws');
|
|
if (pos > 3) region = endpoint.substr(3, pos - 3);
|
|
let forcePathStyle = options.forcePathStyle || false;
|
|
|
|
this.s3[name] = new S3({
|
|
endpoint: 'https://' + endpoint,
|
|
credentials: { accessKeyId, secretAccessKey },
|
|
region,
|
|
signatureVersion: 'v4',
|
|
forcePathStyle,
|
|
});
|
|
|
|
return this.s3[name];
|
|
},
|
|
|
|
getS3Provider: function (name) {
|
|
if (this.s3[name]) {
|
|
return this.s3[name];
|
|
}
|
|
|
|
if (config.s3[name]) {
|
|
return this.setS3Provider(name, config.s3[name]);
|
|
}
|
|
|
|
if (fs.existsSync(`app/modules/s3/${name}.json`)) {
|
|
let action = fs.readJSONSync(`app/modules/s3/${name}.json`);
|
|
return this.setS3Provider(name, action.options);
|
|
}
|
|
|
|
throw new Error(`Couldn't find S3 provider "${name}".`);
|
|
},
|
|
|
|
setJSONWebToken: function (name, options) {
|
|
const jwt = require('jsonwebtoken');
|
|
|
|
options = this.parse(options);
|
|
|
|
let opts = {};
|
|
|
|
if (options.alg) opts.algorithm = options.alg;
|
|
if (options.iss) opts.issuer = options.iss;
|
|
if (options.sub) opts.subject = options.sub;
|
|
if (options.aud) opts.audience = options.aud;
|
|
if (options.jti) opts.jwtid = options.jti;
|
|
if (options.expiresIn) opts.expiresIn = options.expiresIn;
|
|
|
|
return (this.jwt[name] = jwt.sign(
|
|
{
|
|
...options.claims,
|
|
},
|
|
options.key,
|
|
{
|
|
expiresIn: 3600, // use as default (required by google)
|
|
...opts,
|
|
}
|
|
));
|
|
},
|
|
|
|
getJSONWebToken: function (name) {
|
|
if (config.jwt[name]) {
|
|
return this.setJSONWebToken(name, config.jwt[name]);
|
|
}
|
|
|
|
if (fs.existsSync(`app/modules/jwt/${name}.json`)) {
|
|
let action = fs.readJSONSync(`app/modules/jwt/${name}.json`);
|
|
return this.setJSONWebToken(name, action.options);
|
|
}
|
|
|
|
throw new Error(`Couldn't find JSON Web Token "${name}".`);
|
|
},
|
|
|
|
define: async function (cfg, internal) {
|
|
if (cfg.settings) {
|
|
this.settings = clone(cfg.settings);
|
|
}
|
|
|
|
if (cfg.vars) {
|
|
this.set(clone(cfg.vars));
|
|
}
|
|
|
|
if (cfg.meta) {
|
|
this.meta = clone(cfg.meta);
|
|
await validator.init(this, this.meta);
|
|
}
|
|
|
|
if (fs.existsSync('app/modules/global.json')) {
|
|
await this.exec(await fs.readJSON('app/modules/global.json'), true);
|
|
}
|
|
|
|
/*
|
|
debug('body: %o', this.req.body);
|
|
debug('query: %o', this.req.query);
|
|
debug('params: %o', this.req.params);
|
|
debug('headers: %o', this.req.headers);
|
|
debug('cookies: %o', this.req.cookies);
|
|
debug('session: %o', this.req.session);
|
|
*/
|
|
|
|
await this.exec(cfg.exec || cfg, internal);
|
|
},
|
|
|
|
sub: async function (actions, scope) {
|
|
const subApp = new App(this.req, this.res);
|
|
subApp.global = this.global;
|
|
subApp.scope = this.scope.create(scope);
|
|
await subApp.exec(actions, true);
|
|
return subApp.data;
|
|
},
|
|
|
|
exec: async function (actions, internal) {
|
|
if (actions.exec) {
|
|
return this.exec(actions.exec, internal);
|
|
}
|
|
|
|
actions = clone(actions);
|
|
|
|
await this._exec(actions.steps || actions);
|
|
|
|
if (this.error !== false) {
|
|
if (actions.catch) {
|
|
this.scope.set('$_ERROR', this.error.message);
|
|
this.error = false;
|
|
await this._exec(actions.catch, true);
|
|
} else {
|
|
throw this.error;
|
|
}
|
|
}
|
|
|
|
if (!internal && !this.res.headersSent && !this.noOutput) {
|
|
this.res.json(this.data);
|
|
}
|
|
},
|
|
|
|
_exec: async function (steps, ignoreAbort) {
|
|
if (this.res.headersSent) return;
|
|
|
|
if (typeof steps == 'string') {
|
|
return this.exec(await fs.readJSON(`app/modules/${steps}.json`), true);
|
|
}
|
|
|
|
if (this.res.headersSent) {
|
|
// do not execute other steps after headers has been sent
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(steps)) {
|
|
for (let step of steps) {
|
|
await this._exec(step);
|
|
if (this.error) return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (steps.disabled) {
|
|
return;
|
|
}
|
|
|
|
if (steps.action) {
|
|
try {
|
|
let module;
|
|
|
|
if (fs.existsSync(`extensions/server_connect/modules/${steps.module}.js`)) {
|
|
module = require(`../../extensions/server_connect/modules/${steps.module}`);
|
|
} else if (fs.existsSync(`lib/modules/${steps.module}.js`)) {
|
|
module = require(`../modules/${steps.module}`);
|
|
} else {
|
|
throw new Error(`Module ${steps.module} doesn't exist`);
|
|
}
|
|
|
|
if (typeof module[steps.action] != 'function') {
|
|
throw new Error(`Action ${steps.action} doesn't exist in ${steps.module || 'core'}`);
|
|
}
|
|
|
|
debug(`Executing action step ${steps.action}`);
|
|
debug(`options: %O`, steps.options);
|
|
|
|
if (!ignoreAbort && config.abortOnDisconnect && this.req.isDisconnected) {
|
|
throw new Error('Aborted');
|
|
}
|
|
|
|
const data = await module[steps.action].call(this, clone(steps.options), steps.name, steps.meta);
|
|
|
|
if (data instanceof Error) {
|
|
throw data;
|
|
}
|
|
|
|
debug2(`${steps.name || steps.action}: %O`, data);
|
|
|
|
if (steps.name) {
|
|
this.scope.set(steps.name, data);
|
|
|
|
if (steps.output) {
|
|
this.data[steps.name] = data;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debug2(`Error at ${steps.name || steps.action}: %O`, e);
|
|
|
|
this.error = e;
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
parse: function (value, scope) {
|
|
return Parser.parseValue(value, scope || this.scope);
|
|
},
|
|
|
|
parseRequired: function (value, type, err) {
|
|
if (value === undefined) {
|
|
throw new Error(err);
|
|
}
|
|
|
|
let val = Parser.parseValue(value, this.scope);
|
|
|
|
if (type == '*') {
|
|
if (val === undefined) {
|
|
throw new Error(err);
|
|
}
|
|
} else if (type == 'boolean') {
|
|
val = !!val;
|
|
} else if (typeof val != type) {
|
|
throw new Error(err);
|
|
}
|
|
|
|
return val;
|
|
},
|
|
|
|
parseOptional: function (value, type, def) {
|
|
if (value === undefined) return def;
|
|
|
|
let val = Parser.parseValue(value, this.scope);
|
|
|
|
if (type == '*') {
|
|
if (val === undefined) val = def;
|
|
} else if (type == 'boolean') {
|
|
if (val === undefined) {
|
|
val = def;
|
|
} else {
|
|
val = !!val;
|
|
}
|
|
} else if (typeof val != type) {
|
|
val = def;
|
|
}
|
|
|
|
return val;
|
|
},
|
|
|
|
parseSQL: function (sql) {
|
|
if (!sql) return null;
|
|
|
|
['values', 'orders'].forEach((prop) => {
|
|
if (Array.isArray(sql[prop])) {
|
|
sql[prop] = sql[prop].filter((value) => {
|
|
if (!value.condition) return true;
|
|
return !!Parser.parseValue(value.condition, this.scope);
|
|
});
|
|
}
|
|
});
|
|
|
|
if (sql.wheres && sql.wheres.rules) {
|
|
if (sql.wheres.conditional && !Parser.parseValue(sql.wheres.conditional, this.scope)) {
|
|
delete sql.wheres;
|
|
} else {
|
|
sql.wheres.rules = sql.wheres.rules.filter(function filterConditional(rule) {
|
|
if (!rule.rules) return true;
|
|
if (rule.conditional && !Parser.parseValue(rule.conditional, this.scope)) return false;
|
|
rule.rules = rule.rules.filter(filterConditional, this);
|
|
return rule.rules.length;
|
|
}, this);
|
|
|
|
if (!sql.wheres.rules.length) {
|
|
delete sql.wheres;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sql.sub) {
|
|
for (const name in sql.sub) {
|
|
sql.sub[name] = this.parseSQL(sql.sub[name]);
|
|
}
|
|
}
|
|
|
|
return Parser.parseValue(sql, this.scope);
|
|
},
|
|
|
|
isDynamic: function (value) {
|
|
if (typeof value == 'string') {
|
|
return value.includes('{{');
|
|
}
|
|
|
|
if (typeof value == 'object') {
|
|
for (const prop in value) {
|
|
if (this.isDynamic(value[prop])) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
};
|
|
|
|
module.exports = App;
|