Skip to content

Commit 1ca3de2

Browse files
authored
refactor: update salesforce:publish API call to not use APIClient (#106)
* refactor: update salesforce:publish API call to not use APIClient * fix: fix publish API URL * test: remove legacy salesforce:publish tests * fix: fix error message printing * fix: update name of metadata * refactor: zip file using deflate instead of gzip * fix: use adm-zip to zip files for salesforce:publish
1 parent b72f4b7 commit 1ca3de2

File tree

5 files changed

+200
-72
lines changed

5 files changed

+200
-72
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
"@heroku-cli/schema": "^1.0.25",
1111
"@oclif/core": "^2.16.0",
1212
"@oclif/plugin-help": "^5",
13+
"adm-zip": "^0.5.16",
14+
"axios": "^1.9.0",
1315
"open": "^8.4.2",
1416
"tsheredoc": "^1"
1517
},
1618
"devDependencies": {
1719
"@oclif/test": "^2.3.28",
20+
"@types/adm-zip": "^0.5.7",
1821
"@types/mocha": "^10",
1922
"@types/nock": "^11",
2023
"@types/node": "22.14.1",

src/commands/salesforce/publish.ts

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {color} from '@heroku-cli/color'
22
import {flags} from '@heroku-cli/command'
33
import {ux, Args} from '@oclif/core'
4+
import axios from 'axios'
45
import fs from 'fs'
56
import path from 'path'
6-
import {gzipSync} from 'zlib'
77
import Command from '../../lib/base'
88
import * as AppLink from '../../lib/applink/types'
9+
import AdmZip from 'adm-zip'
910

1011
export default class Publish extends Command {
1112
static description = 'publish an app\'s API specification to an authenticated Salesforce org'
@@ -25,6 +26,12 @@ export default class Publish extends Command {
2526
api_spec_file_dir: Args.file({required: true, description: 'path to OpenAPI 3.x spec file (JSON or YAML format)'}),
2627
}
2728

29+
protected createZipArchive = async (files: AppLink.FileEntry[]) => {
30+
const zipArchive = new AdmZip()
31+
files.forEach(file => zipArchive.addFile(file.name, file.content))
32+
return zipArchive.toBuffer()
33+
}
34+
2835
public async run(): Promise<void> {
2936
const {flags, args} = await this.parse(Publish)
3037
const {app, addon, 'client-name': clientName, 'connection-name': connectionName, 'authorization-connected-app-name': authorizationConnectedAppName, 'authorization-permission-set-name': authorizationPermissionSetName, 'metadata-dir': metadataDir} = flags
@@ -82,32 +89,48 @@ export default class Publish extends Command {
8289
}
8390
}
8491

85-
const filesForJson = files.map(file => ({
86-
name: file.name,
87-
content: file.content.toString('base64'),
92+
const compressedContent = await this.createZipArchive(files)
93+
const appRequestContent = {
94+
client_name: clientName,
95+
authorization_connected_app_name: authorizationConnectedAppName,
96+
authorization_permission_set_name: authorizationPermissionSetName,
97+
}
98+
const formData = new FormData()
99+
formData.append('metadata', new Blob([
100+
compressedContent,
101+
], {
102+
type: 'application/zip',
103+
}
104+
))
105+
formData.append('app_request', new Blob([
106+
JSON.stringify(appRequestContent),
107+
], {
108+
type: 'application/json',
88109
}))
89110

90-
const compressedContent = gzipSync(JSON.stringify(filesForJson))
91-
const binaryMetadataZip = compressedContent.toString('base64')
92-
93111
await this.configureAppLinkClient(app, addon)
94112

95-
ux.action.start(`Publishing ${color.app(app)} to ${color.yellow(connectionName)} as ${color.yellow(clientName)}`)
96-
97-
await this.applinkClient.post<AppLink.AppPublish>(
98-
`/addons/${this.addonId}/connections/salesforce/${connectionName}/apps`,
99-
{
100-
headers: {authorization: `Bearer ${this._applinkToken}`},
101-
body: {
102-
app_request: {
103-
client_name: clientName,
104-
authorization_connected_app_name: authorizationConnectedAppName,
105-
authorization_permission_set_name: authorizationPermissionSetName,
106-
},
107-
metadata_zip: binaryMetadataZip,
108-
},
109-
retryAuth: false,
110-
})
113+
const publishURL = `https://${this._applink.defaults.host}/addons/${this.addonId}/connections/salesforce/${connectionName}/apps`
114+
const headers = this._applink.defaults.headers || {}
115+
116+
ux.action.start(`Publishing ${color.app(app)} to ${color.yellow(connectionName)} as ${color.yellow(clientName)} via ${publishURL}`)
117+
118+
await axios.post(publishURL, formData, {
119+
headers: {
120+
accept: 'application/json',
121+
'Content-Type': 'multipart/form-data',
122+
Authorization: `Bearer ${this._applinkToken}`,
123+
'x-addon-sso': headers['x-addon-sso'],
124+
'x-app-uuid': headers['x-app-uuid'],
125+
'User-Agent': headers['user-agent'],
126+
},
127+
}).catch(error => {
128+
if (error.response.data && error.response.data.message) {
129+
ux.error(error.response.data.message, {exit: 1})
130+
} else {
131+
throw error
132+
}
133+
})
111134

112135
ux.action.stop()
113136
}

src/lib/base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {ux} from '@oclif/core'
66
import heredoc from 'tsheredoc'
77

88
export default abstract class extends Command {
9-
private _applink!: APIClient
109
private _addonId!: string
10+
_applink!: APIClient
1111
_addonName!: string
1212
_appId!: string
1313
_applinkToken!: string

test/commands/salesforce/publish.test.ts

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {runCommand} from '../../run-command'
55
import Cmd from '../../../src/commands/salesforce/publish'
66
import {
77
addon,
8-
legacyAddon,
98
sso_response,
109
} from '../../helpers/fixtures'
1110
import stripAnsi from '../../helpers/strip-ansi'
@@ -60,51 +59,6 @@ describe('salesforce:publish', function () {
6059
})
6160
})
6261

63-
context('when config var is set to the legacy HEROKU_INTEGRATION_API_URL', function () {
64-
let integrationApi: nock.Scope
65-
66-
beforeEach(function () {
67-
process.env = {}
68-
api = nock('https://api.heroku.com')
69-
.get('/apps/my-app/addons')
70-
.reply(200, [legacyAddon])
71-
.get('/apps/my-app/config-vars')
72-
.reply(200, {
73-
HEROKU_INTEGRATION_API_URL: 'https://integration-api.heroku.com/addons/01234567-89ab-cdef-0123-456789abcdef',
74-
HEROKU_INTEGRATION_TOKEN: 'token',
75-
})
76-
.get('/apps/my-app/addons/01234567-89ab-cdef-0123-456789abcdef/sso')
77-
.reply(200, sso_response)
78-
integrationApi = nock('https://integration-api.heroku.com')
79-
})
80-
81-
afterEach(function () {
82-
process.env = env
83-
api.done()
84-
integrationApi.done()
85-
nock.cleanAll()
86-
})
87-
88-
it('successfully publishes an API spec file', async function () {
89-
integrationApi
90-
.post('/addons/01234567-89ab-cdef-0123-456789abcdef/connections/salesforce/myorg/apps')
91-
.reply(201, [])
92-
93-
const filePath = `${__dirname}/../../helpers/openapi.json`
94-
95-
await runCommand(Cmd, [
96-
filePath,
97-
'--app=my-app',
98-
'--addon=heroku-integration-vertical-01234',
99-
'--client-name=AccountAPI',
100-
'--connection-name=myorg',
101-
])
102-
103-
expect(stripAnsi(stderr.output)).to.contain('Publishing my-app to myorg as AccountAPI')
104-
expect(stdout.output).to.equal('')
105-
})
106-
})
107-
10862
it('throws an error when API spec file is not found', async function () {
10963
const nonExistentPath = `${__dirname}/non-existent-file.json`
11064

0 commit comments

Comments
 (0)