UNPKG

heroku-kafka

Version:
189 lines (188 loc) 6.77 kB
import memoize from 'lodash.memoize'; class NotFoundError extends Error { statusCode; constructor() { super('Couldn\'t find that addon.'); this.name = 'NotFound'; this.statusCode = 404; Error.captureStackTrace(this, this.constructor); } } class AmbiguousError extends Error { body; matches; statusCode; type; constructor(matches, type) { super(`Ambiguous identifier; multiple matching add-ons found: ${matches.map(match => match.name).join(', ')}.`); this.name = 'AmbiguousError'; this.statusCode = 422; this.body = { id: 'multiple_matches', message: this.message }; this.matches = matches; this.type = type; Error.captureStackTrace(this, this.constructor); } } const addonHeaders = function () { return { Accept: 'application/vnd.heroku+json; version=3.actions', 'Accept-Expansion': 'addon_service,plan', }; }; const attachmentHeaders = function () { return { Accept: 'application/vnd.heroku+json; version=3.actions', 'Accept-Inclusion': 'addon:plan,config_vars', }; }; const appAddon = function (heroku, app, id, options = {}) { const headers = addonHeaders(); return heroku.post('/actions/addons/resolve', { headers, body: { app, addon: id, addon_service: options.addon_service }, }) .then((res) => res.body || res) .then(singularize('addon', options.namespace)); }; const handleNotFound = function (err, resource) { if (err.statusCode === 404 && err.body && err.body.resource === resource) { return true; } throw err; }; const addonResolver = function (heroku, app, id, options = {}) { const headers = addonHeaders(); const getAddon = function (id) { return heroku.post('/actions/addons/resolve', { headers, body: { app: null, addon: id, addon_service: options.addon_service }, }) .then((res) => res.body || res) .then(singularize('addon', options.namespace)); }; if (!app || id.includes('::')) return getAddon(id); return appAddon(heroku, app, id, options) .catch((error) => { if (handleNotFound(error, 'add_on')) return getAddon(id); }); }; /** * Replacing memoize with our own memoization function that works with promises * https://github.com/lodash/lodash/blob/da329eb776a15825c04ffea9fa75ae941ea524af/lodash.js#L10534 */ const memoizePromise = function (func, resolver) { const memoized = function (...args) { const key = resolver.apply(this, args); const { cache } = memoized; if (cache.has(key)) { return cache.get(key); } const result = func.apply(this, args); // eslint-disable-next-line prefer-arrow-callback return result.then(function () { memoized.cache = cache.set(key, result) || cache; return result; }); }; memoized.cache = new memoize.Cache(); return memoized; }; const addon = memoizePromise(addonResolver, (_, app, id, options = {}) => `${app}|${id}|${options.addon_service}`); const singularize = function (type, namespace) { return (matches) => { if (namespace) { matches = matches.filter(m => m.namespace === namespace); } else if (matches.length > 1) { // In cases that aren't specific enough, filter by namespace matches = matches.filter(m => !m.hasOwnProperty('namespace') || m.namespace === null); } switch (matches.length) { case 0: { throw new NotFoundError(); } case 1: { return matches[0]; } default: { throw new AmbiguousError(matches, type); } } }; }; const attachment = function (heroku, app, id, options = {}) { const headers = attachmentHeaders(); function getAttachment(id) { return heroku.post('/actions/addon-attachments/resolve', { headers, body: { app: null, addon_attachment: id, addon_service: options.addon_service }, }) .then((res) => res.body || res) .then(singularize('addon_attachment', options.namespace)) .catch((error) => { handleNotFound(error, 'add_on attachment'); }); } function getAppAddonAttachment(addon, app) { return heroku.get(`/addons/${encodeURIComponent(addon.id)}/addon-attachments`, { headers }) .then((res) => res.body || res) .then(filter(app, options.addon_service)) .then(singularize('addon_attachment', options.namespace)); } let promise; if (!app || id.includes('::')) { promise = getAttachment(id); } else { promise = appAttachment(heroku, app, id, options) .catch((error) => { handleNotFound(error, 'add_on attachment'); }); } // first check to see if there is an attachment matching this app/id combo return promise .then((attachment) => ({ attachment })) .catch((error) => ({ error })) // if no attachment, look up an add-on that matches the id .then((attachOrError) => { const { attachment, error } = attachOrError; if (attachment) return attachment; // If we were passed an add-on slug, there still could be an attachment // to the context app. Try to find and use it so `context_app` is set // correctly in the SSO payload. if (app) { return addon(heroku, app, id, options) .then((addon) => getAppAddonAttachment(addon, app)) .catch((addonError) => { if (error) throw error; throw addonError; }); } if (error) throw error; throw new NotFoundError(); }); }; const appAttachment = function (heroku, app, id, options = {}) { const headers = attachmentHeaders(); return heroku.post('/actions/addon-attachments/resolve', { headers, body: { app, addon_attachment: id, addon_service: options.addon_service }, }) .then((res) => res.body || res) .then(singularize('addon_attachment', options.namespace)); }; const filter = function (app, addonService) { return (attachments) => attachments.filter((attachment) => { if (attachment.app.name !== app) { return false; } if (addonService && attachment.addon_service.name !== addonService) { return false; } return true; }); }; export { addon, appAddon, appAttachment, attachment, };