heroku-kafka
Version:
heroku plugin to manage heroku kafka
189 lines (188 loc) • 6.77 kB
JavaScript
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, };