@axway/api-builder-plugin-dc-mbs
Version:
Mobile Backend Services connector
229 lines (218 loc) • 7.17 kB
JavaScript
/**
* Translates the standard API Builder "sel" and "unsel" syntax, that is based
* on Mongo's, into the syntax supported by MBS query.
*
* @param {object} Model - The Model.
* @param {object|string} source - The source `sel` or `unsel` parameter.
* @param {object} [options={}] - Options
* @param {boolean} [options.includeId=false] = Includes or exludes the 'id' from
* the list of fields.
* @return {object} An object with an `all` array of translated fields.
*/
function translateSelOrUnsel(Model, source, options = {}) {
const result = {
all: []
};
const type = typeof source;
if (type === 'string') {
const parts = source.split(/\s*,\s*/);
for (const part of parts) {
if (part && !result.all.includes(part)) {
const translated = translateFieldForPayload(Model, part);
result.all.push(translated);
}
}
} else if (type === 'object') {
for (const key in source) {
if (source.hasOwnProperty(key) && source[key] === 1) {
const translated = translateFieldForPayload(Model, key);
// not possible to have duplicate keys, so guaranteed unique
result.all.push(translated);
}
}
} else {
return null;
}
// add or remove 'id'
if (options.includeId && !result.all.includes('id')) {
result.all.push('id');
} else if (!options.includeId && result.all.includes('id')) {
result.all.splice(result.all.indexOf('id'), 1);
}
if (result.all.length) {
return result;
}
return null;
}
/**
* Translates the standard API Builder "order" syntax, that is based on Mongo's
* into the syntax supported by MBS queries. The `source` can be a string,
* e.g. "name,email", or an object, e.g. {"name": 1, "email": -1}. If the
* object's property is 1, it will be ascending, otherwise, it will be descending.
*
* @param {object} Model - The Model.
* @param {object|string} source - The source `order` parameter.
* @return {object} A CSV representing the MBS order query parameter.
*/
function translateOrderToCSV(Model, source) {
if (typeof source === 'string') {
// not sure this is right - order is always a dictionary
return source;
}
const result = [];
for (const key in source) {
// So we don't translate inherited properties
if (source.hasOwnProperty(key)) {
const translated = translateFieldForPayload(Model, key);
if (source[key] === 1) {
result.push(translated);
} else if (source[key] === -1) {
result.push(`-${translated}`);
}
}
}
return result.join(',');
}
/**
* Checks that the limit is valid. Throws an error if out of range.
*
* @param {number} limit - The limit value to check.
*/
function validateLimitOption(limit) {
if (typeof limit !== 'number' || limit > 1000 || limit < 1) {
const err = new Error('Invalid limit parameter; value must be in a valid range of 1~1000');
err.status = 400;
throw err;
}
}
/**
* Checks that `Model` has the `field`. Throws if `field` is invalid.
*
* @param {object} Model - The Model.
* @param {string} field - The field name to check.
*/
function checkField(Model, field) {
if (!Model.fields.hasOwnProperty(field) && field !== 'id') {
const err = new Error(`Unknown field: ${field}`);
err.status = 400;
throw err;
}
}
/**
* Checks that the query is valid. Throws an error if invalid.
*
* @param {object} where - The `where` query parameter.
*/
function validateWhereOption(where) {
for (const prop in where) {
if (where.hasOwnProperty(prop)) {
const val = where[prop];
if (prop === '$regex' && val.substr(0, 3) === '^.*') {
throw new Error('$like queries cannot begin with a wildcard');
} else if (typeof val === 'object') {
validateWhereOption(val);
}
}
}
}
/**
* Translates fields to supported MBS data types. If a Model field is of type "Date",
* and it is defined in `fields`, and it is an instance of a Date object, then
* the value will be translated to a string representation of it in ISO 8601 date-time format
* (without milliseconds), e.g. from '1984-01-01T00:00:00.000Z' to 1984-01-01T00:00:00Z.
*
* @param {object} fields - A dictionary of fields.
* @param {object} [translated={}] - Internal use.
* @return {object} The translated `fields`.
*/
function translateFields(fields, translated = {}) {
for (const prop in fields) {
const value = fields[prop];
const isInstanceofDate = value instanceof Date;
const isInstanceofNumber = value instanceof Number;
const isInstanceofArray = value instanceof Array;
const isNull = value === null;
if (isInstanceofArray) {
// translates each element in the `value` array. it will build
// an object from the array index, so need Object.values.
translated[prop] = Object.values(translateFields(value));
} else if (typeof value === 'object'
&& !isInstanceofDate
&& !isInstanceofNumber
&& !isNull) {
// if this prop:value is an actual object (i.e. not Date, Number, Array,
// or null), then recurse the translate.
translated[prop] = translateFields(value);
} else if (isInstanceofDate) {
// if this prop:value is a `Date` field or is an instance of a date, then
// convert the date to ISO string, and trim off the trailing milliseconds.
translated[prop] = value.toISOString().replace(/\.[\d]{3}Z$/, 'Z');
} else {
translated[prop] = value;
}
}
return translated;
}
/**
* Translates a field for internal use, translating from a "soft" name such as
* alias or insensitive case, to a "hard" field name used for persistence. This
* will check that `field` is an actual field in the Model and will throw if it
* is not.
*
* @param {object} Model - The Model.
* @param {string} field - The field name to translate.
*
* @return {string} The translated field name.
*/
function translateFieldForPayload(Model, field) {
checkField(Model, field);
const translated = Object.keys(Model.translateKeysForPayload({ [field]: 1 }));
// There is no need to check `translated.length`. At this point, we know that
// `field` is a string, that it is an actual field in the Model, and that
// translateKeysForPayload will return an object.
return translated[0];
}
/**
* Returns a new Error message. It will log the `err`.
* Errors which relate to "5xx" errors from MBS will have their message hidden.
* "4xx" errors will retain their message
*
* @param {object} connector - The connector.
* @param {Error} err - An error to log.
* @param {string} prefix - A prefix to apply to the error message
*
* @example
* getTranslatedError(this, err);
* @return {Error} An internal Error message.
*/
function getTranslatedError(connector, err, prefix) {
const status = err.statusCode || 500;
let msg;
if (status < 500) {
msg = err.reason || err.message;
if (msg.indexOf('Error: ') === 0) {
msg = msg.substring(7);
}
} else {
msg = 'Internal error';
}
if (prefix) {
msg = `${prefix}: ${msg}`;
}
if (status >= 500) {
connector.logger.trace(msg, err);
}
const newErr = new Error(msg);
newErr.status = status;
return newErr;
}
module.exports = {
translateSelOrUnsel,
translateOrderToCSV,
validateLimitOption,
validateWhereOption,
checkField,
translateFields,
translateWhere: translateFields,
getTranslatedError
};