lib/resolve.js
'use strict'
const _ = require('lodash')
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': headers,
'body': {'app': app, 'addon': id, 'addon_service': options.addon_service}
})
.then(singularize('addon', options.namespace))
}
const handleNotFound = function (err, resource) {
if (err.statusCode === 404 && err.body && err.body.resource === resource) {
return true
} else {
throw err
}
}
exports.appAddon = appAddon
const addonResolver = function (heroku, app, id, options = {}) {
const headers = addonHeaders()
let getAddon = function (id) {
return heroku.post('/actions/addons/resolve', {
'headers': headers,
'body': {'app': null, 'addon': id, 'addon_service': options.addon_service}
})
.then(singularize('addon', options.namespace))
}
if (!app || id.includes('::')) return getAddon(id)
return appAddon(heroku, app, id, options)
.catch(function (err) { if (handleNotFound(err, '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) {
var memoized = function () {
const args = arguments
const key = resolver.apply(this, args)
const cache = memoized.cache
if (cache.has(key)) {
return cache.get(key)
}
const result = func.apply(this, args)
return result.then(function () {
memoized.cache = cache.set(key, result) || cache
return result
})
}
memoized.cache = new _.memoize.Cache()
return memoized
}
exports.addon = memoizePromise(addonResolver, (_, app, id, options = {}) => `${app}|${id}|${options.addon_service}`)
function NotFound () {
Error.call(this)
Error.captureStackTrace(this, this.constructor)
this.name = this.constructor.name
this.statusCode = 404
this.message = 'Couldn\'t find that addon.'
}
function AmbiguousError (matches, type) {
Error.call(this)
Error.captureStackTrace(this, this.constructor)
this.name = this.constructor.name
this.statusCode = 422
this.message = `Ambiguous identifier; multiple matching add-ons found: ${matches.map((match) => match.name).join(', ')}.`
this.body = {'id': 'multiple_matches', 'message': this.message}
this.matches = matches
this.type = type
}
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 NotFound()
case 1:
return matches[0]
default:
throw new AmbiguousError(matches, type)
}
}
}
exports.attachment = function (heroku, app, id, options = {}) {
const headers = attachmentHeaders()
function getAttachment (id) {
return heroku.post('/actions/addon-attachments/resolve', {
'headers': headers, 'body': {'app': null, 'addon_attachment': id, 'addon_service': options.addon_service}
}).then(singularize('addon_attachment', options.namespace))
.catch(function (err) { handleNotFound(err, 'add_on attachment') })
}
function getAppAddonAttachment (addon, app) {
return heroku.get(`/addons/${encodeURIComponent(addon.id)}/addon-attachments`, {headers})
.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(function (err) { handleNotFound(err, 'add_on attachment') })
}
// first check to see if there is an attachment matching this app/id combo
return promise
.then(function (attachment) {
return {attachment}
})
.catch(function (error) {
return {error}
})
// if no attachment, look up an add-on that matches the id
.then((attachOrError) => {
let {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.
else if (app) {
return exports.addon(heroku, app, id, options)
.then((addon) => getAppAddonAttachment(addon, app))
.catch((addonError) => {
if (error) throw error
throw addonError
})
} else {
if (error) throw error
throw new NotFound()
}
})
}
const appAttachment = function (heroku, app, id, options = {}) {
const headers = attachmentHeaders()
return heroku.post('/actions/addon-attachments/resolve', {
'headers': headers, 'body': {'app': app, 'addon_attachment': id, 'addon_service': options.addon_service}
}).then(singularize('addon_attachment', options.namespace))
}
exports.appAttachment = appAttachment
const filter = function (app, addonService) {
return attachments => {
return attachments.filter(attachment => {
if (attachment.app.name !== app) {
return false
}
if (addonService && attachment.addon_service.name !== addonService) {
return false
}
return true
})
}
}