orange-orm
Version:
Object Relational Mapper
557 lines (493 loc) • 15 kB
JavaScript
const createPatch = require('../client/createPatch');
const emptyFilter = require('../emptyFilter');
const negotiateRawSqlFilter = require('../table/column/negotiateRawSqlFilter');
let getMeta = require('./getMeta');
let isSafe = Symbol();
let _allowedOps = {
and: true,
or: true,
not: true,
AND: true,
OR: true,
NOT: true,
equal: true,
eq: true,
EQ: true,
notEqual: true,
ne: true,
NE: true,
lessThan: true,
lt: true,
LT: true,
lessThanOrEqual: true,
le: true,
LE: true,
greaterThan: true,
gt: true,
GT: true,
greaterThanOrEqual: true,
ge: true,
GE: true,
between: true,
in: true,
IN: true,
startsWith: true,
iStartsWith: true,
endsWith: true,
iEndsWith: true,
contains: true,
iContains: true,
iEqual: true,
iEq: true,
ieq: true,
IEQ: true,
exists: true,
all: true,
any: true,
none: true,
where: true,
sum: true,
avg: true,
max: true,
min: true,
count: true,
groupSum: true,
groupAvg: true,
groupMax: true,
groupMin: true,
groupCount: true,
_aggregate: true,
self: true,
};
function _executePath(context, ...rest) {
const _ops = {
and: emptyFilter.and.bind(null, context),
or: emptyFilter.or.bind(null, context),
not: emptyFilter.not.bind(null, context),
AND: emptyFilter.and.bind(null, context),
OR: emptyFilter.or.bind(null, context),
NOT: emptyFilter.not.bind(null, context),
};
return executePath(...rest);
async function executePath({ table, JSONFilter, baseFilter, customFilters = {}, request, response, readonly, disableBulkDeletes, isHttp, client }) {
let allowedOps = { ..._allowedOps, insert: !readonly, ...extractRelations(getMeta(table)) };
let ops = { ..._ops, ...getCustomFilterPaths(customFilters), getManyDto, getMany, aggregate, distinct, count, delete: _delete, cascadeDelete, update, replace };
let res = await parseFilter(JSONFilter, table);
if (res === undefined)
return {};
else
return res;
function parseFilter(json, table) {
if (isFilter(json)) {
let subFilters = [];
let anyAllNone = tryGetAnyAllNone(json.path, table);
if (anyAllNone) {
const arg0 = json.args[0];
if (isHttp && arg0 !== undefined)
validateArgs(arg0);
const f = arg0 === undefined
? anyAllNone(context)
: anyAllNone(context, x => parseFilter(arg0, x));
if(!('isSafe' in f))
f.isSafe = isSafe;
return f;
}
else {
for (let i = 0; i < json.args.length; i++) {
subFilters.push(parseFilter(json.args[i], nextTable(json.path, table)));
}
}
return executePath(json.path, subFilters);
}
else if (Array.isArray(json)) {
const result = [];
for (let i = 0; i < json.length; i++) {
result.push(parseFilter(json[i], table));
}
return result;
}
else if (isColumnRef(json)) {
return resolveColumnRef(table, json.__columnRef);
}
return json;
function tryGetAnyAllNone(path, table) {
const parts = path.split('.');
for (let i = 0; i < parts.length; i++) {
table = table[parts[i]];
}
let ops = new Set(['all', 'any', 'none', 'where', '_aggregate']);
// let ops = new Set(['all', 'any', 'none', 'where']);
let last = parts[parts.length - 1];
if (last === 'count' && parts.length > 1)
ops.add('count');
if (ops.has(last) || (table && (table._primaryColumns || (table.any && table.all))))
return table;
}
function executePath(path, args) {
if (path in ops) {
if (isHttp)
validateArgs(args);
let op = ops[path].apply(null, args);
if (op.then)
return op.then((o) => {
setSafe(o);
return o;
});
setSafe(op);
return op;
}
let pathArray = path.split('.');
let target = table;
let op = pathArray[pathArray.length - 1];
if (!allowedOps[op] && isHttp) {
let e = new Error('Disallowed operator ' + op);
// @ts-ignore
e.status = 403;
throw e;
}
for (let i = 0; i < pathArray.length; i++) {
target = target[pathArray[i]];
}
if (!target) {
const left = args && args[0];
if (left) {
target = left;
for (let i = 0; i < pathArray.length; i++) {
target = target[pathArray[i]];
}
if (target) {
let res = target.apply(null, [context].concat(args.slice(1)));
setSafe(res);
return res;
}
}
throw new Error(`Method '${path}' does not exist`);
}
let res = target.apply(null, [context, ...args]);
setSafe(res);
return res;
}
}
async function invokeBaseFilter() {
if (typeof baseFilter === 'function') {
const res = await baseFilter.apply(null, [bindDb(client), request, response]);
if (!res)
return;
const JSONFilter = JSON.parse(JSON.stringify(res));
//@ts-ignore
return executePath({ table, JSONFilter, request, response });
}
else
return;
}
function getCustomFilterPaths(customFilters) {
return getLeafNames(customFilters);
function getLeafNames(obj, result = {}, current = 'customFilters.') {
for (let p in obj) {
if (typeof obj[p] === 'object' && obj[p] !== null)
getLeafNames(obj[p], result, current + p + '.');
else
result[current + p] = resolveFilter.bind(null, obj[p]);
}
return result;
}
async function resolveFilter(fn, ...args) {
const context = { db: bindDb(client), request, response };
let res = fn.apply(null, [context, ...args]);
if (res.then)
res = await res;
const JSONFilter = JSON.parse(JSON.stringify(res));
//@ts-ignore
return executePath({ table, JSONFilter, request, response });
}
}
function nextTable(path, table) {
path = path.split('.');
let ops = new Set(['all', 'any', 'none', 'count']);
let last = path.slice(-1)[0];
if (ops.has(last)) {
for (let i = 0; i < path.length - 1; i++) {
table = table[path[i]];
}
return table;
}
else {
let lastObj = table;
for (let i = 0; i < path.length; i++) {
if (lastObj)
lastObj = lastObj[path[i]];
}
if (lastObj?._shallow)
return lastObj._shallow;
else return table;
}
}
async function _delete(filter) {
if (readonly || disableBulkDeletes) {
let e = new Error('Bulk deletes are not allowed. Parameter "disableBulkDeletes" must be true.');
// @ts-ignore
e.status = 403;
throw e;
}
filter = negotiateFilter(filter);
const _baseFilter = await invokeBaseFilter();
if (_baseFilter)
filter = filter.and(context, _baseFilter);
let args = [context, filter].concat(Array.prototype.slice.call(arguments).slice(1));
return table.delete.apply(null, args);
}
async function cascadeDelete(filter) {
if (readonly || disableBulkDeletes) {
const e = new Error('Bulk deletes are not allowed. Parameter "disableBulkDeletes" must be true.');
// @ts-ignore
e.status = 403;
throw e;
}
filter = negotiateFilter(filter);
const _baseFilter = await invokeBaseFilter();
if (_baseFilter)
filter = filter.and(context, _baseFilter);
let args = [context, filter].concat(Array.prototype.slice.call(arguments).slice(1));
return table.cascadeDelete.apply(null, args);
}
function negotiateFilter(filter) {
if (filter)
return negotiateRawSqlFilter(context, filter, table, true);
else
return emptyFilter;
}
async function count(filter, strategy) {
validateStrategy(table, strategy);
filter = negotiateFilter(filter);
const _baseFilter = await invokeBaseFilter();
if (_baseFilter)
filter = filter.and(context, _baseFilter);
let args = [context, filter].concat(Array.prototype.slice.call(arguments).slice(1));
return table.count.apply(null, args);
}
async function getManyDto(filter, strategy) {
validateStrategy(table, strategy);
filter = negotiateFilter(filter);
const _baseFilter = await invokeBaseFilter();
if (_baseFilter)
filter = filter.and(context, _baseFilter);
let args = [context, filter].concat(Array.prototype.slice.call(arguments).slice(1));
await negotiateWhereAndAggregate(strategy);
return table.getManyDto.apply(null, args);
}
async function replace(subject, strategy = { insertAndForget: true }) {
validateStrategy(table, strategy);
const refinedStrategy = objectToStrategy(subject, {}, table);
const JSONFilter2 = {
path: 'getManyDto',
args: [subject, refinedStrategy]
};
const originals = await executePath({ table, JSONFilter: JSONFilter2, baseFilter, customFilters, request, response, readonly, disableBulkDeletes, isHttp, client });
const meta = getMeta(table);
const patch = createPatch(originals, Array.isArray(subject) ? subject : [subject], meta);
const { changed } = await table.patch(context, patch, { strategy });
if (Array.isArray(subject))
return changed;
else
return changed[0];
}
async function update(subject, whereStrategy, strategy = { insertAndForget: true }) {
validateStrategy(table, strategy);
const refinedWhereStrategy = objectToStrategy(subject, whereStrategy, table);
const JSONFilter2 = {
path: 'getManyDto',
args: [null, refinedWhereStrategy]
};
const rows = await executePath({ table, JSONFilter: JSONFilter2, baseFilter, customFilters, request, response, readonly, disableBulkDeletes, isHttp, client });
const originals = new Array(rows.length);
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
originals[i] = { ...row };
for (let p in subject) {
row[p] = subject[p];
}
}
const meta = getMeta(table);
const patch = createPatch(originals, rows, meta);
const { changed } = await table.patch(context, patch, { strategy });
return changed;
}
function objectToStrategy(object, whereStrategy, table, strategy = {}) {
strategy = { ...whereStrategy, ...strategy };
if (Array.isArray(object)) {
for (let i = 0; i < object.length; i++) {
objectToStrategy(object[i], table, strategy);
}
return;
}
for (let name in object) {
const relation = table[name]?._relation;
if (relation && !relation.columns) {//notJoin, that is one or many
strategy[name] = {};
objectToStrategy(object[name], whereStrategy?.[name], table[name], strategy[name]);
}
else
strategy[name] = true;
}
return strategy;
}
async function aggregate(filter, strategy) {
validateStrategy(table, strategy);
filter = negotiateFilter(filter);
const _baseFilter = await invokeBaseFilter();
if (_baseFilter)
filter = filter.and(context, _baseFilter);
let args = [context, filter].concat(Array.prototype.slice.call(arguments).slice(1));
await negotiateWhereAndAggregate(strategy);
return table.aggregate.apply(null, args);
}
async function distinct(filter, strategy) {
validateStrategy(table, strategy);
filter = negotiateFilter(filter);
const _baseFilter = await invokeBaseFilter();
if (_baseFilter)
filter = filter.and(context, _baseFilter);
let args = [context, filter].concat(Array.prototype.slice.call(arguments).slice(1));
await negotiateWhereAndAggregate(strategy);
return table.distinct.apply(null, args);
}
async function negotiateWhereAndAggregate(strategy) {
if (typeof strategy !== 'object')
return;
for (let name in strategy) {
const target = strategy[name];
if (isFilter(target))
strategy[name] = await parseFilter(strategy[name], table);
else
await negotiateWhereAndAggregate(strategy[name]);
}
}
async function getMany(filter, strategy) {
validateStrategy(table, strategy);
filter = negotiateFilter(filter);
const _baseFilter = await invokeBaseFilter();
if (_baseFilter)
filter = filter.and(context, _baseFilter);
let args = [context, filter].concat(Array.prototype.slice.call(arguments).slice(1));
await negotiateWhereAndAggregate(strategy);
return table.getMany.apply(null, args);
}
}
function validateStrategy(table, strategy) {
if (!strategy || !table)
return;
for (let p in strategy) {
validateOffset(strategy);
validateLimit(strategy);
validateOrderBy(table, strategy);
validateStrategy(table[p], strategy[p]);
}
}
function validateLimit(strategy) {
if (!('limit' in strategy) || Number.isInteger(strategy.limit))
return;
const e = new Error('Invalid limit: ' + strategy.limit);
// @ts-ignore
e.status = 400;
}
function validateOffset(strategy) {
if (!('offset' in strategy) || Number.isInteger(strategy.offset))
return;
const e = new Error('Invalid offset: ' + strategy.offset);
// @ts-ignore
e.status = 400;
throw e;
}
function validateOrderBy(table, strategy) {
if (!('orderBy' in strategy) || !table)
return;
let orderBy = strategy.orderBy;
if (!Array.isArray(orderBy))
orderBy = [orderBy];
orderBy.reduce(validate, []);
function validate(_, element) {
let parts = element.split(' ').filter(x => {
x = x.toLowerCase();
return (!(x === '' || x === 'asc' || x === 'desc'));
});
for (let p of parts) {
let col = table[p];
if (!(col && col.equal)) {
const e = new Error('Unknown column: ' + p);
// @ts-ignore
e.status = 400;
throw e;
}
}
}
}
function validateArgs() {
for (let i = 0; i < arguments.length; i++) {
const filter = arguments[i];
if (!filter)
continue;
if (filter && filter.isSafe === isSafe)
continue;
if (filter.sql || typeof (filter) === 'string') {
const e = new Error('Raw filters are disallowed');
// @ts-ignore
e.status = 403;
throw e;
}
if (Array.isArray(filter))
for (let i = 0; i < filter.length; i++) {
validateArgs(filter[i]);
}
}
}
function isFilter(json) {
return json instanceof Object && 'path' in json && 'args' in json;
}
function isColumnRef(json) {
return json instanceof Object && typeof json.__columnRef === 'string';
}
function resolveColumnRef(table, path) {
let current = table;
const parts = path.split('.');
for (let i = 0; i < parts.length; i++) {
if (current)
current = current[parts[i]];
}
if (!current || typeof current._toFilterArg !== 'function') {
let e = new Error(`Column reference '${path}' is invalid`);
// @ts-ignore
e.status = 400;
throw e;
}
return current;
}
function setSafe(o) {
if (o instanceof Object)
Object.defineProperty(o, 'isSafe', {
value: isSafe,
enumerable: false
});
}
function extractRelations(obj) {
let flattened = {};
function helper(relations) {
Object.keys(relations).forEach(key => {
flattened[key] = true;
if (typeof relations[key] === 'object' && Object.keys(relations[key]?.relations)?.length > 0) {
helper(relations[key].relations);
}
});
}
helper(obj.relations);
return flattened;
}
function bindDb(client) {
var domain = context;
let p = domain.run(() => true);
function run(fn) {
return p.then(domain.run.bind(domain, fn));
}
return client({ transaction: run });
}
}
module.exports = _executePath;