Skip to content

accept attachment name or url to get addon config #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ fastify.get('/accounts', async function (request, reply) {
const applinkAddon = request.sdk.addons.applink;

logger.info(`Getting org '${orgName}' connection from Heroku Applink add-on...`);
const anotherOrg = await applinkAddon.getConnection(orgName);
const anotherOrg = await applinkAddon.getAuthorization(orgName);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might need to rethink the example a bit here given the change to use a developer name instead of org name.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agree. Best to be consistent.


logger.info(`Querying org '${orgName}' (${anotherOrg.id}) Accounts...`);
try {
Expand Down
50 changes: 26 additions & 24 deletions src/add-ons/heroku-applink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,47 @@
import { HttpRequestUtil } from "../utils/request";
import { OrgImpl } from "../sdk/org";
import { Org } from "../index";
import {
resolveAddonConfigByAttachment,
resolveAddonConfigByUrl,
} from "~/utils/addon-config";

const HTTP_REQUEST = new HttpRequestUtil();

/**
* Get stored Salesforce or Data Cloud org user credentials for given name or alias.
* @param name or alias
* Get stored Salesforce or Data Cloud org user credentials for given developer name or alias.
* @param developerName or alias
* @param attachmentNameOrUrl Either an attachment name (e.g. "HEROKU_APPLINK") or a full URL. Defaults to "HEROKU_APPLINK"
* @returns Org
*/
export async function getConnection(name: string): Promise<Org> {
if (!name) {
throw Error(`Connection name not provided`);
export async function getAuthorization(
developerName: string,
attachmentNameOrUrl = "HEROKU_APPLINK"
): Promise<Org> {
if (!developerName) {
throw Error(`Developer name not provided`);
}

const addonEndpoint =
process.env.HEROKU_APPLINK_API_URL ||
process.env.HEROKU_APPLINK_STAGING_API_URL;
if (!addonEndpoint) {
throw Error(
`Heroku Applink add-on not provisioned on app or endpoint not provided`
);
// Check if the attachmentNameOrUrl is a URL by attempting to parse it
let resolveConfigByUrl = false;
try {
new URL(attachmentNameOrUrl);
resolveConfigByUrl = true;
} catch {
resolveConfigByUrl = false;
}

const addonAuthToken = process.env.HEROKU_APPLINK_TOKEN;
if (!addonAuthToken) {
throw Error(
`Heroku Applink add-on not provisioned on app or authorization token not found`
);
}
const config = resolveConfigByUrl
? resolveAddonConfigByUrl(attachmentNameOrUrl)
: resolveAddonConfigByAttachment(attachmentNameOrUrl);

const authUrl = `${addonEndpoint}/invocations/authorization`;
const authUrl = `${config.apiUrl}/authorizations/${developerName}`;
const opts = {
method: "POST",
method: "GET",
headers: {
Authorization: `Bearer ${addonAuthToken}`,
Authorization: `Bearer ${config.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
org_name: name,
}),
retry: {
limit: 1,
},
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ContextImpl } from "./sdk/context.js";
import { InvocationEventImpl } from "./sdk/invocation-event.js";
import { LoggerImpl } from "./sdk/logger.js";
import { QueryOperation } from "jsforce/lib/api/bulk";
import { getConnection } from "./add-ons/heroku-applink.js";
import { getAuthorization } from "./add-ons/heroku-applink.js";

const CONTENT_TYPE_HEADER = "Content-Type";
const X_CLIENT_CONTEXT_HEADER = "x-client-context";
Expand All @@ -25,7 +25,7 @@ export function init() {
return {
addons: {
applink: {
getConnection,
getAuthorization,
},
},
dataCloud: {
Expand Down
57 changes: 57 additions & 0 deletions src/utils/addon-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2025. Favor: update date everywhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I'll handle that in a separate pr to avoid noise.

* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

interface AddonConfig {
apiUrl: string;
token: string;
}

export function resolveAddonConfigByAttachment(
attachment: string
): AddonConfig {
const apiUrl = process.env[`${attachment.toUpperCase()}_API_URL`];
const token = process.env[`${attachment.toUpperCase()}_TOKEN`];

if (!apiUrl || !token) {
throw Error(
`Heroku Applink config not found under attachment ${attachment}`
);
}

return {
apiUrl,
token,
};
}

export function resolveAddonConfigByUrl(url: string): AddonConfig {
// Find the environment variable ending with _API_URL that matches the given URL
const envVarEntries = Object.entries(process.env);
const matchingApiUrlEntry = envVarEntries.find(
([key, value]) =>
key.endsWith("_API_URL") && value.toLowerCase() === url.toLowerCase()
);

if (!matchingApiUrlEntry) {
throw Error(`Heroku Applink config not found for API URL: ${url}`);
}

// Extract the prefix from the API_URL environment variable name
const [envVarName] = matchingApiUrlEntry;
const prefix = envVarName.slice(0, -"_API_URL".length); // Remove '_API_URL' suffix

// Look for corresponding token
const token = process.env[`${prefix}_TOKEN`];
if (!token) {
throw Error(`Heroku Applink token not found for API URL: ${url}`);
}

return {
apiUrl: matchingApiUrlEntry[1],
token,
};
}
122 changes: 122 additions & 0 deletions test/utils/addon-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { expect } from "chai";
import {
resolveAddonConfigByAttachment,
resolveAddonConfigByUrl,
} from "../../src/utils/addon-config";

const ATTACHMENT = "HEROKU_APPLINK";

describe("resolveAddonConfigByAttachment", () => {
let originalEnv: NodeJS.ProcessEnv;

beforeEach(() => {
originalEnv = { ...process.env };
});

afterEach(() => {
process.env = originalEnv;
});

it("finds config for specified attachment", () => {
process.env.HEROKU_APPLINK_API_URL = "https://api.example.com";
process.env.HEROKU_APPLINK_TOKEN = "default-token";

const config = resolveAddonConfigByAttachment(ATTACHMENT);
expect(config.apiUrl).to.equal("https://api.example.com");
expect(config.token).to.equal("default-token");
});

it("finds config ignoring case of attachment name", () => {
process.env.HEROKU_APPLINK_API_URL = "https://api.example.com";
process.env.HEROKU_APPLINK_TOKEN = "default-token";

const config = resolveAddonConfigByAttachment(ATTACHMENT.toLowerCase());
expect(config.apiUrl).to.equal("https://api.example.com");
expect(config.token).to.equal("default-token");
});

it("throws if API_URL config not found", () => {
process.env.HEROKU_APPLINK_TOKEN = "token";
// APPLINK_API_URL intentionally not set

expect(() => resolveAddonConfigByAttachment(ATTACHMENT)).to.throw(
"Heroku Applink config not found under attachment HEROKU_APPLINK"
);
});

it("throws if TOKEN config not found", () => {
process.env.HEROKU_APPLINK_API_URL = "https://api.example.com";
// APPLINK_TOKEN intentionally not set

expect(() => resolveAddonConfigByAttachment(ATTACHMENT)).to.throw(
"Heroku Applink config not found under attachment HEROKU_APPLINK"
);
});
});

describe("resolveAddonConfigByUrl", () => {
let originalEnv: NodeJS.ProcessEnv;

beforeEach(() => {
originalEnv = { ...process.env };
});

afterEach(() => {
process.env = originalEnv;
});

it("finds config for matching URL", () => {
const testUrl = "https://api.example.com";
process.env.HEROKU_APPLINK_API_URL = testUrl;
process.env.HEROKU_APPLINK_TOKEN = "test-token";

const config = resolveAddonConfigByUrl(testUrl);
expect(config.apiUrl).to.equal(testUrl);
expect(config.token).to.equal("test-token");
});

it("finds config ignoring url case", () => {
const testUrl = "https://api.example.com";
process.env.HEROKU_APPLINK_API_URL = testUrl;
process.env.HEROKU_APPLINK_TOKEN = "test-token";

const config = resolveAddonConfigByUrl(testUrl.toUpperCase());
expect(config.apiUrl).to.equal(testUrl);
expect(config.token).to.equal("test-token");
});

it("finds custom prefix config for matching URL", () => {
const testUrl = "https://custom.example.com";
process.env.CUSTOM_ATTACHMENT_API_URL = testUrl;
process.env.CUSTOM_ATTACHMENT_TOKEN = "custom-token";

const config = resolveAddonConfigByUrl(testUrl);
expect(config.apiUrl).to.equal(testUrl);
expect(config.token).to.equal("custom-token");
});

it("throws if no matching URL is found", () => {
const testUrl = "https://nonexistent.example.com";

expect(() => resolveAddonConfigByUrl(testUrl)).to.throw(
`Heroku Applink config not found for API URL: ${testUrl}`
);
});

it("throws if matching URL found but no corresponding token", () => {
const testUrl = "https://api.example.com";
process.env.SOME_API_URL = testUrl;
// SOME_TOKEN intentionally not set

expect(() => resolveAddonConfigByUrl(testUrl)).to.throw(
`Heroku Applink token not found for API URL: ${testUrl}`
);
});
});
Loading