UNPKG

5.94 kBJavaScriptView Raw
1'use strict'
2
3const _ = require('lodash')
4
5const addonHeaders = function () {
6 return {
7 'Accept': 'application/vnd.heroku+json; version=3.actions',
8 'Accept-Expansion': 'addon_service,plan'
9 }
10}
11
12const attachmentHeaders = function () {
13 return {
14 'Accept': 'application/vnd.heroku+json; version=3.actions',
15 'Accept-Inclusion': 'addon:plan,config_vars'
16 }
17}
18
19const appAddon = function (heroku, app, id, options = {}) {
20 const headers = addonHeaders()
21 return heroku.post('/actions/addons/resolve', {
22 'headers': headers,
23 'body': {'app': app, 'addon': id, 'addon_service': options.addon_service}
24 })
25 .then(singularize('addon', options.namespace))
26}
27
28const handleNotFound = function (err, resource) {
29 if (err.statusCode === 404 && err.body && err.body.resource === resource) {
30 return true
31 } else {
32 throw err
33 }
34}
35
36exports.appAddon = appAddon
37
38const addonResolver = function (heroku, app, id, options = {}) {
39 const headers = addonHeaders()
40
41 let getAddon = function (id) {
42 return heroku.post('/actions/addons/resolve', {
43 'headers': headers,
44 'body': {'app': null, 'addon': id, 'addon_service': options.addon_service}
45 })
46 .then(singularize('addon', options.namespace))
47 }
48
49 if (!app || id.includes('::')) return getAddon(id)
50
51 return appAddon(heroku, app, id, options)
52 .catch(function (err) { if (handleNotFound(err, 'add_on')) return getAddon(id) })
53}
54
55/**
56 * Replacing memoize with our own memoization function that works with promises
57 * https://github.com/lodash/lodash/blob/da329eb776a15825c04ffea9fa75ae941ea524af/lodash.js#L10534
58 */
59const memoizePromise = function (func, resolver) {
60 var memoized = function () {
61 const args = arguments
62 const key = resolver.apply(this, args)
63 const cache = memoized.cache
64
65 if (cache.has(key)) {
66 return cache.get(key)
67 }
68
69 const result = func.apply(this, args)
70
71 return result.then(function () {
72 memoized.cache = cache.set(key, result) || cache
73 return result
74 })
75 }
76 memoized.cache = new _.memoize.Cache()
77 return memoized
78}
79
80exports.addon = memoizePromise(addonResolver, (_, app, id, options = {}) => `${app}|${id}|${options.addon_service}`)
81
82function NotFound () {
83 Error.call(this)
84 Error.captureStackTrace(this, this.constructor)
85 this.name = this.constructor.name
86
87 this.statusCode = 404
88 this.message = 'Couldn\'t find that addon.'
89}
90
91function AmbiguousError (matches, type) {
92 Error.call(this)
93 Error.captureStackTrace(this, this.constructor)
94 this.name = this.constructor.name
95
96 this.statusCode = 422
97 this.message = `Ambiguous identifier; multiple matching add-ons found: ${matches.map((match) => match.name).join(', ')}.`
98 this.body = {'id': 'multiple_matches', 'message': this.message}
99 this.matches = matches
100 this.type = type
101}
102
103const singularize = function (type, namespace) {
104 return (matches) => {
105 if (namespace) {
106 matches = matches.filter(m => m.namespace === namespace)
107 } else if (matches.length > 1) {
108 // In cases that aren't specific enough, filter by namespace
109 matches = matches.filter(m => !m.hasOwnProperty('namespace') || m.namespace === null)
110 }
111 switch (matches.length) {
112 case 0:
113 throw new NotFound()
114 case 1:
115 return matches[0]
116 default:
117 throw new AmbiguousError(matches, type)
118 }
119 }
120}
121exports.attachment = function (heroku, app, id, options = {}) {
122 const headers = attachmentHeaders()
123
124 function getAttachment (id) {
125 return heroku.post('/actions/addon-attachments/resolve', {
126 'headers': headers, 'body': {'app': null, 'addon_attachment': id, 'addon_service': options.addon_service}
127 }).then(singularize('addon_attachment', options.namespace))
128 .catch(function (err) { handleNotFound(err, 'add_on attachment') })
129 }
130
131 function getAppAddonAttachment (addon, app) {
132 return heroku.get(`/addons/${encodeURIComponent(addon.id)}/addon-attachments`, {headers})
133 .then(filter(app, options.addon_service))
134 .then(singularize('addon_attachment', options.namespace))
135 }
136
137 let promise
138 if (!app || id.includes('::')) {
139 promise = getAttachment(id)
140 } else {
141 promise = appAttachment(heroku, app, id, options)
142 .catch(function (err) { handleNotFound(err, 'add_on attachment') })
143 }
144
145 // first check to see if there is an attachment matching this app/id combo
146 return promise
147 .then(function (attachment) {
148 return {attachment}
149 })
150 .catch(function (error) {
151 return {error}
152 })
153 // if no attachment, look up an add-on that matches the id
154 .then((attachOrError) => {
155 let {attachment, error} = attachOrError
156
157 if (attachment) return attachment
158
159 // If we were passed an add-on slug, there still could be an attachment
160 // to the context app. Try to find and use it so `context_app` is set
161 // correctly in the SSO payload.
162 else if (app) {
163 return exports.addon(heroku, app, id, options)
164 .then((addon) => getAppAddonAttachment(addon, app))
165 .catch((addonError) => {
166 if (error) throw error
167 throw addonError
168 })
169 } else {
170 if (error) throw error
171 throw new NotFound()
172 }
173 })
174}
175
176const appAttachment = function (heroku, app, id, options = {}) {
177 const headers = attachmentHeaders()
178 return heroku.post('/actions/addon-attachments/resolve', {
179 'headers': headers, 'body': {'app': app, 'addon_attachment': id, 'addon_service': options.addon_service}
180 }).then(singularize('addon_attachment', options.namespace))
181}
182
183exports.appAttachment = appAttachment
184
185const filter = function (app, addonService) {
186 return attachments => {
187 return attachments.filter(attachment => {
188 if (attachment.app.name !== app) {
189 return false
190 }
191
192 if (addonService && attachment.addon_service.name !== addonService) {
193 return false
194 }
195
196 return true
197 })
198 }
199}