@resin/pinejs
Version:
Pine.js is a sophisticated rules-driven API engine that enables you to define rules in a structured subset of English. Those rules are used in order for Pine.js to generate a database schema and the associated [OData](http://www.odata.org/) API. This make
191 lines (179 loc) • 5.82 kB
text/typescript
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import * as AbstractSQLCompiler from '@resin/abstract-sql-compiler';
import { ODataBinds, odataNameToSqlName } from '@resin/odata-to-abstract-sql';
import deepFreeze = require('deep-freeze');
import * as memoize from 'memoizee';
import memoizeWeak = require('memoizee/weak');
import * as env from '../config-loader/env';
import { SqlCompilationError } from './errors';
import * as sbvrUtils from './sbvr-utils';
import { ODataRequest } from './uri-parser';
const getMemoizedCompileRule = memoize(
(engine: AbstractSQLCompiler.Engines) =>
memoizeWeak(
(abstractSqlQuery: AbstractSQLCompiler.AbstractSqlQuery) => {
const sqlQuery = AbstractSQLCompiler[engine].compileRule(
abstractSqlQuery,
);
const modifiedFields = AbstractSQLCompiler[engine].getModifiedFields(
abstractSqlQuery,
);
if (modifiedFields != null) {
deepFreeze(modifiedFields);
}
return {
sqlQuery,
modifiedFields,
};
},
{ max: env.cache.abstractSqlCompiler.max },
),
{ primitive: true },
);
export const compileRequest = (request: ODataRequest) => {
if (request.abstractSqlQuery != null) {
const { engine } = request;
if (engine == null) {
throw new SqlCompilationError('No database engine specified');
}
try {
const { sqlQuery, modifiedFields } = getMemoizedCompileRule(engine)(
request.abstractSqlQuery,
);
request.sqlQuery = sqlQuery;
request.modifiedFields = modifiedFields;
} catch (err) {
sbvrUtils.api[request.vocabulary].logger.error(
'Failed to compile abstract sql: ',
request.abstractSqlQuery,
err,
);
throw new SqlCompilationError(err);
}
}
return request;
};
export const resolveOdataBind = (odataBinds: ODataBinds, value: any) => {
if (typeof value === 'object' && value != null && value.bind != null) {
[, value] = odataBinds[value.bind];
}
return value;
};
export const getAndCheckBindValues = (
request: Required<
Pick<ODataRequest, 'vocabulary' | 'odataBinds' | 'values' | 'engine'>
>,
bindings: AbstractSQLCompiler.Binding[],
) => {
const { odataBinds, values, engine } = request;
const sqlModelTables = sbvrUtils.getAbstractSqlModel(request).tables;
return Bluebird.map(bindings, async (binding) => {
let fieldName: string = '';
let field: { dataType: string };
let value: any;
if (binding[0] === 'Bind') {
const bindValue = binding[1];
if (Array.isArray(bindValue)) {
let tableName;
[tableName, fieldName] = bindValue;
const referencedName = tableName + '.' + fieldName;
value = values[referencedName];
if (value === undefined) {
value = values[fieldName];
}
value = resolveOdataBind(odataBinds, value);
const sqlTableName = odataNameToSqlName(tableName);
const sqlFieldName = odataNameToSqlName(fieldName);
const maybeField = sqlModelTables[sqlTableName].fields.find(
(f) => f.fieldName === sqlFieldName,
);
if (maybeField == null) {
throw new Error(`Could not find field '${fieldName}'`);
}
field = maybeField;
} else if (Number.isInteger(bindValue)) {
if (bindValue >= odataBinds.length) {
console.error(
`Invalid binding number '${bindValue}' for binds: `,
odataBinds,
);
throw new Error('Invalid binding');
}
let dataType;
[dataType, value] = odataBinds[bindValue];
field = { dataType };
} else if (typeof bindValue === 'string') {
if (!odataBinds.hasOwnProperty(bindValue)) {
console.error(
`Invalid binding '${bindValue}' for binds: `,
odataBinds,
);
throw new Error('Invalid binding');
}
let dataType;
[dataType, value] = odataBinds[bindValue];
field = { dataType };
} else {
throw new Error(`Unknown binding: ${binding}`);
}
} else {
let dataType;
[dataType, value] = binding;
field = { dataType };
}
if (value === undefined) {
throw new Error(`Bind value cannot be undefined: ${binding}`);
}
try {
return await AbstractSQLCompiler[engine].dataTypeValidate(value, field);
} catch (err) {
err.message = `"${fieldName}" ${err.message}`;
throw err;
}
});
};
const checkModifiedFields = (
referencedFields: AbstractSQLCompiler.ReferencedFields,
modifiedFields: AbstractSQLCompiler.ModifiedFields,
) => {
const refs = referencedFields[modifiedFields.table];
// If there are no referenced fields of the modified table then the rule is not affected
if (refs == null) {
return false;
}
// If there are no specific fields listed then that means they were all modified (ie insert/delete) and so the rule can be affected
if (modifiedFields.fields == null) {
return true;
}
// Otherwise check if there are any matching fields to see if the rule is affected
return _.intersection(refs, modifiedFields.fields).length > 0;
};
export const isRuleAffected = (
rule: AbstractSQLCompiler.SqlRule,
request?: ODataRequest,
) => {
// If there is no abstract sql query then nothing was modified
if (request == null || request.abstractSqlQuery == null) {
return false;
}
// If for some reason there are no referenced fields known for the rule then we just assume it may have been modified
if (rule.referencedFields == null) {
return true;
}
const { modifiedFields } = request;
// If we can't get any modified fields we assume the rule may have been modified
if (modifiedFields == null) {
console.warn(
`Could not determine the modified table/fields info for '${request.method}' to ${request.vocabulary}`,
request.abstractSqlQuery,
);
return true;
}
if (Array.isArray(modifiedFields)) {
return modifiedFields.some(
_.partial(checkModifiedFields, rule.referencedFields),
);
}
return checkModifiedFields(rule.referencedFields, modifiedFields);
};