UNPKG

@axway/api-builder-plugin-dc-mbs

Version:

Mobile Backend Services connector

229 lines (218 loc) 7.17 kB
/** * 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 };