@heroku/sdk 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/data/backup/capture-and-wait.d.ts +2 -2
- package/dist/resources/data/internal/resolve-pg-database.d.ts +30 -14
- package/dist/resources/data/internal/resolve-pg-database.js +74 -30
- package/dist/resources/data/restore/restore-and-wait.d.ts +2 -2
- package/dist/resources/extensions/platform.d.ts +1 -0
- package/dist/resources/extensions/platform.js +1 -0
- package/dist/resources/platform/add-on/describe.d.ts +0 -5
- package/dist/resources/platform/add-on/describe.js +0 -5
- package/dist/resources/platform/add-on/index.d.ts +2 -4
- package/dist/resources/platform/add-on/index.js +2 -4
- package/dist/resources/platform/add-on/list-plans.d.ts +1 -4
- package/dist/resources/platform/add-on/list-plans.js +1 -4
- package/dist/resources/platform/add-on/resolve.d.ts +10 -38
- package/dist/resources/platform/add-on/resolve.js +21 -87
- package/dist/resources/platform/add-on/types.d.ts +8 -22
- package/dist/resources/platform/add-on/upgrade.d.ts +2 -3
- package/dist/resources/platform/add-on/upgrade.js +2 -3
- package/dist/resources/platform/add-on-attachment/index.d.ts +6 -0
- package/dist/resources/platform/add-on-attachment/index.js +6 -0
- package/dist/resources/platform/add-on-attachment/resolve.d.ts +80 -0
- package/dist/resources/platform/add-on-attachment/resolve.js +78 -0
- package/package.json +5 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BackupCreateResult, TransferInfoByAppResult } from '@heroku/types/data';
|
|
2
2
|
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ResolvedPgDatabase } from '../internal/resolve-pg-database.js';
|
|
4
4
|
import type { WaitForTransferOptions } from '../wait-for-transfer.js';
|
|
5
5
|
import { Poller } from '../../../utils/poller.js';
|
|
6
6
|
export type CaptureAndWaitOptions = WaitForTransferOptions & {
|
|
@@ -8,7 +8,7 @@ export type CaptureAndWaitOptions = WaitForTransferOptions & {
|
|
|
8
8
|
* Progress hooks fired once before capturing the backup `poller.onStart(addon)`
|
|
9
9
|
* and once after the backup is created `poller.onStop(addon)`.
|
|
10
10
|
*/
|
|
11
|
-
capturePoller?: Poller<
|
|
11
|
+
capturePoller?: Poller<ResolvedPgDatabase>;
|
|
12
12
|
/**
|
|
13
13
|
* Progress hooks fired once before waiting for the transfer to
|
|
14
14
|
* complete `poller.onStart(backup)` and once after the transfer
|
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
2
|
-
import type {
|
|
3
|
-
export type
|
|
2
|
+
import type { ResolveAddonAttachmentOptions, ResolvedAddOnAttachment } from '../../platform/add-on-attachment/resolve.js';
|
|
3
|
+
export type ResolvedPgDatabase = ResolvedAddOnAttachment['addon'];
|
|
4
|
+
export type ResolvePgDatabaseOptions = ResolveAddonAttachmentOptions & {
|
|
5
|
+
appIdentity?: string;
|
|
4
6
|
input?: string;
|
|
5
7
|
};
|
|
6
8
|
/**
|
|
7
|
-
* Resolve a Heroku Postgres database add-on from
|
|
8
|
-
* shapes the platform recognizes:
|
|
9
|
+
* Resolve a Heroku Postgres database add-on from an attachment identity.
|
|
9
10
|
*
|
|
11
|
+
* `input` selects the attachment as follows:
|
|
10
12
|
* - omitted → the `DATABASE_URL` attachment on `appIdentity`.
|
|
11
|
-
* - `
|
|
12
|
-
*
|
|
13
|
-
* - `parent-app::branch-name`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* - `parent-app::attachment-or-config-var` (e.g. `my-app::DATABASE_URL`)
|
|
14
|
+
* → an attachment or config var on `parent-app`.
|
|
15
|
+
* - `parent-app::branch-name` (e.g. `my-app::my-branch`) → branch add-on
|
|
16
|
+
* name on `parent-app`.
|
|
17
|
+
* - Both `::` forms are handled the same way here: we split on the first
|
|
18
|
+
* `::` ourselves and send `app`/`addon` as separate fields, which
|
|
19
|
+
* works for either shape.
|
|
20
|
+
* - any other string (attachment name, config var, UUID, or
|
|
21
|
+
* globally-unique add-on name) → routed through `resolveAddonAttachment`.
|
|
18
22
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
23
|
+
* This function only uses the attachment resolver. To resolve a database
|
|
24
|
+
* through the add-on resolve endpoint, call `resolveAddon` directly.
|
|
25
|
+
*
|
|
26
|
+
* The returned add-on is the identity nested on the attachment (`id`,
|
|
27
|
+
* `name`, `app`, and the plan stub), not a full `ResolvedAddOn`. To get
|
|
28
|
+
* that shape, call `resolveAddon` with the returned add-on's `id`.
|
|
29
|
+
*
|
|
30
|
+
* If the resolver finds multiple matches that all point at the same add-on
|
|
31
|
+
* (same `addon.id`/attachment `app.id`) and whose own `_URL` config var
|
|
32
|
+
* resolves to the same value on the app, they're treated as equivalent
|
|
33
|
+
* (e.g. the same database attached under two names) rather than ambiguous.
|
|
34
|
+
*
|
|
35
|
+
* Throws `AddonAttachmentNotFoundError` (or `AddonAttachmentAmbiguousError`)
|
|
36
|
+
* from the underlying resolver. Throws if no input is given and no `appIdentity`
|
|
21
37
|
* is available to default the attachment lookup to.
|
|
22
38
|
*/
|
|
23
|
-
export declare function resolvePgDatabase(ctx: Pick<ResourceCtx, 'platform'>, options?: ResolvePgDatabaseOptions): Promise<
|
|
39
|
+
export declare function resolvePgDatabase(ctx: Pick<ResourceCtx, 'platform'>, options?: ResolvePgDatabaseOptions): Promise<ResolvedPgDatabase>;
|
|
@@ -1,50 +1,94 @@
|
|
|
1
1
|
import createDebug from 'debug';
|
|
2
|
-
import {
|
|
2
|
+
import { AddonAttachmentAmbiguousError, resolveAddonAttachment } from '../../platform/add-on-attachment/resolve.js';
|
|
3
3
|
const debug = createDebug('heroku:sdk:resources:database');
|
|
4
4
|
const DEFAULT_PG_ATTACHMENT = 'DATABASE_URL';
|
|
5
|
+
const PG_ADDON_SERVICE = process.env.HEROKU_POSTGRESQL_ADDON_NAME ?? process.env.HEROKU_DATA_SERVICE ?? 'heroku-postgresql';
|
|
5
6
|
/**
|
|
6
|
-
* Resolve a Heroku Postgres database add-on from
|
|
7
|
-
* shapes the platform recognizes:
|
|
7
|
+
* Resolve a Heroku Postgres database add-on from an attachment identity.
|
|
8
8
|
*
|
|
9
|
+
* `input` selects the attachment as follows:
|
|
9
10
|
* - omitted → the `DATABASE_URL` attachment on `appIdentity`.
|
|
10
|
-
* - `
|
|
11
|
-
*
|
|
12
|
-
* - `parent-app::branch-name`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* - `parent-app::attachment-or-config-var` (e.g. `my-app::DATABASE_URL`)
|
|
12
|
+
* → an attachment or config var on `parent-app`.
|
|
13
|
+
* - `parent-app::branch-name` (e.g. `my-app::my-branch`) → branch add-on
|
|
14
|
+
* name on `parent-app`.
|
|
15
|
+
* - Both `::` forms are handled the same way here: we split on the first
|
|
16
|
+
* `::` ourselves and send `app`/`addon` as separate fields, which
|
|
17
|
+
* works for either shape.
|
|
18
|
+
* - any other string (attachment name, config var, UUID, or
|
|
19
|
+
* globally-unique add-on name) → routed through `resolveAddonAttachment`.
|
|
17
20
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
21
|
+
* This function only uses the attachment resolver. To resolve a database
|
|
22
|
+
* through the add-on resolve endpoint, call `resolveAddon` directly.
|
|
23
|
+
*
|
|
24
|
+
* The returned add-on is the identity nested on the attachment (`id`,
|
|
25
|
+
* `name`, `app`, and the plan stub), not a full `ResolvedAddOn`. To get
|
|
26
|
+
* that shape, call `resolveAddon` with the returned add-on's `id`.
|
|
27
|
+
*
|
|
28
|
+
* If the resolver finds multiple matches that all point at the same add-on
|
|
29
|
+
* (same `addon.id`/attachment `app.id`) and whose own `_URL` config var
|
|
30
|
+
* resolves to the same value on the app, they're treated as equivalent
|
|
31
|
+
* (e.g. the same database attached under two names) rather than ambiguous.
|
|
32
|
+
*
|
|
33
|
+
* Throws `AddonAttachmentNotFoundError` (or `AddonAttachmentAmbiguousError`)
|
|
34
|
+
* from the underlying resolver. Throws if no input is given and no `appIdentity`
|
|
20
35
|
* is available to default the attachment lookup to.
|
|
21
36
|
*/
|
|
22
37
|
export async function resolvePgDatabase(ctx, options = {}) {
|
|
23
38
|
const { appIdentity, input, ...rest } = options;
|
|
24
|
-
if (!input) {
|
|
25
|
-
|
|
26
|
-
throw new Error('resolvePgDatabase requires either input or appIdentity to default to DATABASE_URL.');
|
|
27
|
-
}
|
|
28
|
-
debug('resolve branch=default-attachment app=%s attachment=%s', appIdentity, DEFAULT_PG_ATTACHMENT);
|
|
29
|
-
return resolveAddonByAttachment(ctx, appIdentity, DEFAULT_PG_ATTACHMENT, rest);
|
|
39
|
+
if (!input && !appIdentity) {
|
|
40
|
+
throw new Error('resolvePgDatabase requires either input or appIdentity to default to DATABASE_URL.');
|
|
30
41
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
const databaseReference = input ?? DEFAULT_PG_ATTACHMENT;
|
|
43
|
+
const { addon, app } = databaseReference.includes('::')
|
|
44
|
+
? parseBranchReference(databaseReference, appIdentity)
|
|
45
|
+
: { addon: databaseReference, app: appIdentity };
|
|
46
|
+
debug('resolve input=%s addon=%s app=%s', input ?? '<default>', addon, app ?? '<global>');
|
|
47
|
+
try {
|
|
48
|
+
const attachment = await resolveAddonAttachment(ctx, app, addon, { ...rest, addonService: PG_ADDON_SERVICE });
|
|
49
|
+
return attachment.addon;
|
|
35
50
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof AddonAttachmentAmbiguousError) {
|
|
53
|
+
const equivalent = collapseIfEquivalent(error.matches);
|
|
54
|
+
if (equivalent) {
|
|
55
|
+
const config = await ctx.platform.configVar.infoForApp(equivalent[0].app.name);
|
|
56
|
+
if (allMatchSameUrl(equivalent, config)) {
|
|
57
|
+
return equivalent[0].addon;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
39
62
|
}
|
|
40
|
-
debug('resolve branch=bare-identity input=%s app=%s', input, appIdentity ?? '<global>');
|
|
41
|
-
return resolveAddon(ctx, input, { ...rest, appIdentity });
|
|
42
63
|
}
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Narrows ambiguous matches to a same-add-on group worth comparing by
|
|
66
|
+
* config var value. Returns `undefined` (not worth a config-vars round
|
|
67
|
+
* trip) unless every match shares the same add-on and attachment app.
|
|
68
|
+
*/
|
|
69
|
+
function collapseIfEquivalent(matches) {
|
|
70
|
+
const [first] = matches;
|
|
71
|
+
const sameAddon = matches.every(match => match.addon.id === first.addon.id && match.app.id === first.app.id);
|
|
72
|
+
return sameAddon ? matches : undefined;
|
|
73
|
+
}
|
|
74
|
+
/** The first config var name declared on an attachment that holds its connection URL. */
|
|
75
|
+
function urlConfigVarName(configVars) {
|
|
76
|
+
return configVars.find(name => name.endsWith('_URL'));
|
|
77
|
+
}
|
|
78
|
+
/** Whether every match's own URL config var resolves to the same value in `config`. */
|
|
79
|
+
function allMatchSameUrl(matches, config) {
|
|
80
|
+
const firstVarName = urlConfigVarName(matches[0].config_vars);
|
|
81
|
+
const firstValue = firstVarName && config[firstVarName];
|
|
82
|
+
if (!firstValue) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return matches.every(match => {
|
|
86
|
+
const varName = urlConfigVarName(match.config_vars);
|
|
87
|
+
return varName !== undefined && config[varName] === firstValue;
|
|
88
|
+
});
|
|
45
89
|
}
|
|
46
90
|
function parseBranchReference(reference, fallbackApp) {
|
|
47
|
-
const match = reference.match(/^(
|
|
91
|
+
const match = reference.match(/^(.+?)::(.+)$/);
|
|
48
92
|
if (match) {
|
|
49
93
|
return { addon: match[2], app: match[1] };
|
|
50
94
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RestoreCreateResult, TransferInfoByAppResult } from '@heroku/types/data';
|
|
2
2
|
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ResolvedPgDatabase } from '../internal/resolve-pg-database.js';
|
|
4
4
|
import type { WaitForTransferOptions } from '../wait-for-transfer.js';
|
|
5
5
|
import { Poller } from '../../../utils/poller.js';
|
|
6
6
|
export type RestoreAndWaitOptions = WaitForTransferOptions & {
|
|
@@ -13,7 +13,7 @@ export type RestoreAndWaitOptions = WaitForTransferOptions & {
|
|
|
13
13
|
* `poller.onStart(addon)` and once after the restore is
|
|
14
14
|
* created `poller.onStop(addon)`.
|
|
15
15
|
*/
|
|
16
|
-
restorePoller?: Poller<
|
|
16
|
+
restorePoller?: Poller<ResolvedPgDatabase>;
|
|
17
17
|
/**
|
|
18
18
|
* Progress hooks fired once before waiting for the restore to
|
|
19
19
|
* complete `poller.onStart(restore)` and once after the restore
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { addOnAttachmentExtensions } from '../platform/add-on-attachment/index.js';
|
|
1
2
|
export { addOnExtensions } from '../platform/add-on/index.js';
|
|
2
3
|
export { appExtensions, type AppInfo, type DiffAppsOptions, type DiffRow, type PipelineCouplingDetail, } from '../platform/app/index.js';
|
|
3
4
|
export { containerExtensions, type DockerReleasesBatchUpdateOpts, NotAContainerAppError, type ReleaseDockerImagesResult, type RemoveProcessTypesOpts, } from '../platform/container/index.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { addOnAttachmentExtensions } from '../platform/add-on-attachment/index.js';
|
|
1
2
|
export { addOnExtensions } from '../platform/add-on/index.js';
|
|
2
3
|
export { appExtensions, } from '../platform/app/index.js';
|
|
3
4
|
export { containerExtensions, NotAContainerAppError, } from '../platform/container/index.js';
|
|
@@ -8,11 +8,6 @@ import type { DescribedAddOn, ResolveAddonOptions } from './types.js';
|
|
|
8
8
|
* - Returns a copy with `plan.price` set from `billed_price` (so the
|
|
9
9
|
* value reflects any grandfathered/contract pricing).
|
|
10
10
|
*
|
|
11
|
-
* If `appIdentity` is provided and the platform returns 404 (resource
|
|
12
|
-
* `add_on`), the resolve falls back to a global lookup. This handles
|
|
13
|
-
* the case where the add-on belongs to a different app than the one
|
|
14
|
-
* supplied.
|
|
15
|
-
*
|
|
16
11
|
* Requests the `version=3.sdk` accept variant with `addon_service,plan`
|
|
17
12
|
* expansion so the resolved add-on includes the full `Plan` shape
|
|
18
13
|
* (`price.unit`, etc.) needed to render pricing.
|
|
@@ -7,11 +7,6 @@ import { resolveAddonInternal } from './resolve.js';
|
|
|
7
7
|
* - Returns a copy with `plan.price` set from `billed_price` (so the
|
|
8
8
|
* value reflects any grandfathered/contract pricing).
|
|
9
9
|
*
|
|
10
|
-
* If `appIdentity` is provided and the platform returns 404 (resource
|
|
11
|
-
* `add_on`), the resolve falls back to a global lookup. This handles
|
|
12
|
-
* the case where the add-on belongs to a different app than the one
|
|
13
|
-
* supplied.
|
|
14
|
-
*
|
|
15
10
|
* Requests the `version=3.sdk` accept variant with `addon_service,plan`
|
|
16
11
|
* expansion so the resolved add-on includes the full `Plan` shape
|
|
17
12
|
* (`price.unit`, etc.) needed to render pricing.
|
|
@@ -8,21 +8,19 @@ export { destroyAndWait } from './destroy-and-wait.js';
|
|
|
8
8
|
export { AddonAmbiguousError, AddonConfirmationRequiredError, AddonNotFoundError, AddonProvisioningFailedError, } from './errors.js';
|
|
9
9
|
export { listPlans, listPlansForAddon, type ListPlansForAddonOptions } from './list-plans.js';
|
|
10
10
|
export { formatPlanPriceLabel, type FormatPlanPriceLabelOptions, type PlanPriceBreakdown, type PlanPriceKind, priceForPlan, } from './pricing.js';
|
|
11
|
-
export {
|
|
12
|
-
export type { AddOnOptions, CreateAndWaitOptions, DescribedAddOn, DestroyAndWaitOptions, ResolveAddonOptions, ResolvedAddOn,
|
|
11
|
+
export { resolveAddon } from './resolve.js';
|
|
12
|
+
export type { AddOnOptions, CreateAndWaitOptions, DescribedAddOn, DestroyAndWaitOptions, ResolveAddonOptions, ResolvedAddOn, UpgradeAddOnOptions, WaitForProvisioningOptions, } from './types.js';
|
|
13
13
|
export { upgrade } from './upgrade.js';
|
|
14
14
|
export { waitForProvisioning } from './wait-for-provisioning.js';
|
|
15
15
|
export declare const addOnExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "addOn", {
|
|
16
16
|
createAndWait: (appIdentity: string, body: AddOnCreateOpts, options?: CreateAndWaitOptions) => Promise<AddOn>;
|
|
17
17
|
describe: (addonIdentity: string, options?: ResolveAddonOptions) => Promise<import("./types.js").DescribedAddOn>;
|
|
18
|
-
describeAttachment: (appIdentity: string, attachmentName: string, options?: AddOnOptions) => Promise<import("./types.js").ResolvedAddOnAttachment>;
|
|
19
18
|
destroyAndWait: (appIdentity: string, addonIdentity: string, options?: DestroyAndWaitOptions) => Promise<AddOn>;
|
|
20
19
|
formatPlanPriceLabel: typeof formatPlanPriceLabel;
|
|
21
20
|
listPlans: (serviceIdentity: string, options?: AddOnOptions) => Promise<import("@heroku/types/3.sdk").Plan[]>;
|
|
22
21
|
listPlansForAddon: (addonIdentity: string, options?: ListPlansForAddonOptions) => Promise<import("@heroku/types/3.sdk").Plan[]>;
|
|
23
22
|
priceForPlan: typeof priceForPlan;
|
|
24
23
|
resolve: (addonIdentity: string, options?: ResolveAddonOptions) => Promise<import("./types.js").ResolvedAddOn>;
|
|
25
|
-
resolveByAttachment: (appIdentity: string, attachmentName: string, options?: AddOnOptions) => Promise<import("./types.js").ResolvedAddOn>;
|
|
26
24
|
upgrade: (addonIdentity: string, plan: string, options?: UpgradeAddOnOptions) => Promise<AddOn>;
|
|
27
25
|
waitForProvisioning: (addon: AddOn, options?: WaitForProvisioningOptions) => Promise<AddOn>;
|
|
28
26
|
}>;
|
|
@@ -4,7 +4,7 @@ import { describeAddon } from './describe.js';
|
|
|
4
4
|
import { destroyAndWait } from './destroy-and-wait.js';
|
|
5
5
|
import { listPlans, listPlansForAddon } from './list-plans.js';
|
|
6
6
|
import { formatPlanPriceLabel, priceForPlan } from './pricing.js';
|
|
7
|
-
import {
|
|
7
|
+
import { resolveAddon } from './resolve.js';
|
|
8
8
|
import { upgrade } from './upgrade.js';
|
|
9
9
|
import { waitForProvisioning } from './wait-for-provisioning.js';
|
|
10
10
|
export { createAndWait } from './create-and-wait.js';
|
|
@@ -13,20 +13,18 @@ export { destroyAndWait } from './destroy-and-wait.js';
|
|
|
13
13
|
export { AddonAmbiguousError, AddonConfirmationRequiredError, AddonNotFoundError, AddonProvisioningFailedError, } from './errors.js';
|
|
14
14
|
export { listPlans, listPlansForAddon } from './list-plans.js';
|
|
15
15
|
export { formatPlanPriceLabel, priceForPlan, } from './pricing.js';
|
|
16
|
-
export {
|
|
16
|
+
export { resolveAddon } from './resolve.js';
|
|
17
17
|
export { upgrade } from './upgrade.js';
|
|
18
18
|
export { waitForProvisioning } from './wait-for-provisioning.js';
|
|
19
19
|
export const addOnExtensions = extendResource('platform', 'addOn', ctx => ({
|
|
20
20
|
createAndWait: (appIdentity, body, options) => createAndWait(ctx, appIdentity, body, options),
|
|
21
21
|
describe: (addonIdentity, options) => describeAddon(ctx, addonIdentity, options),
|
|
22
|
-
describeAttachment: (appIdentity, attachmentName, options) => describeAttachment(ctx, appIdentity, attachmentName, options),
|
|
23
22
|
destroyAndWait: (appIdentity, addonIdentity, options) => destroyAndWait(ctx, appIdentity, addonIdentity, options),
|
|
24
23
|
formatPlanPriceLabel,
|
|
25
24
|
listPlans: (serviceIdentity, options) => listPlans(ctx, serviceIdentity, options),
|
|
26
25
|
listPlansForAddon: (addonIdentity, options) => listPlansForAddon(ctx, addonIdentity, options),
|
|
27
26
|
priceForPlan,
|
|
28
27
|
resolve: (addonIdentity, options) => resolveAddon(ctx, addonIdentity, options),
|
|
29
|
-
resolveByAttachment: (appIdentity, attachmentName, options) => resolveAddonByAttachment(ctx, appIdentity, attachmentName, options),
|
|
30
28
|
upgrade: (addonIdentity, plan, options) => upgrade(ctx, addonIdentity, plan, options),
|
|
31
29
|
waitForProvisioning: (addon, options) => waitForProvisioning(ctx, addon, options),
|
|
32
30
|
}));
|
|
@@ -23,10 +23,7 @@ export declare function listPlans(ctx: Pick<ResourceCtx, 'platform'>, serviceIde
|
|
|
23
23
|
* the dance of fetching the add-on, plucking its service id/name, and
|
|
24
24
|
* passing it through.
|
|
25
25
|
*
|
|
26
|
-
* `addonIdentity` accepts the same forms as `resolveAddon
|
|
27
|
-
* - a UUID
|
|
28
|
-
* - a globally-unique add-on name
|
|
29
|
-
* - a namespaced credential reference (`name::SECONDARY`)
|
|
26
|
+
* `addonIdentity` accepts the same forms as `resolveAddon`.
|
|
30
27
|
*
|
|
31
28
|
* Pass `appIdentity` via `options` when the add-on identity is
|
|
32
29
|
* scoped to an app (same semantics as `resolveAddon`).
|
|
@@ -19,10 +19,7 @@ export async function listPlans(ctx, serviceIdentity, options = {}) {
|
|
|
19
19
|
* the dance of fetching the add-on, plucking its service id/name, and
|
|
20
20
|
* passing it through.
|
|
21
21
|
*
|
|
22
|
-
* `addonIdentity` accepts the same forms as `resolveAddon
|
|
23
|
-
* - a UUID
|
|
24
|
-
* - a globally-unique add-on name
|
|
25
|
-
* - a namespaced credential reference (`name::SECONDARY`)
|
|
22
|
+
* `addonIdentity` accepts the same forms as `resolveAddon`.
|
|
26
23
|
*
|
|
27
24
|
* Pass `appIdentity` via `options` when the add-on identity is
|
|
28
25
|
* scoped to an app (same semantics as `resolveAddon`).
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
2
2
|
import type { PlatformClient } from '../../../services/platform.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ResolveAddonOptions, ResolvedAddOn } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Resolve a Platform add-on by identity.
|
|
6
6
|
*
|
|
7
7
|
* The add-on identity may be:
|
|
8
|
-
* -
|
|
9
|
-
* - a globally-unique name (`postgres-curved-12345`)
|
|
10
|
-
* -
|
|
8
|
+
* - an add-on UUID (`d5e3f2a4-...`)
|
|
9
|
+
* - a globally-unique add-on name (`postgres-curved-12345`)
|
|
10
|
+
* - an add-on service slug (`heroku-postgresql`)
|
|
11
|
+
* - an add-on plan slug (`heroku-postgresql:essential-0`)
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* It may also be an add-on attachment identity:
|
|
14
|
+
* - an attachment UUID (`2a4d5e3f-...`), name (`DATABASE`), or config
|
|
15
|
+
* var (`DATABASE_URL`). These identities require `appIdentity`.
|
|
16
|
+
* - an attachment name or config var prefixed with the app name
|
|
17
|
+
* (`my-app::DATABASE`, `my-app::DATABASE_URL`).
|
|
16
18
|
*
|
|
17
19
|
* `addonService` (e.g. `heroku-postgresql`) is filtered client-side after
|
|
18
20
|
* the resolve. The platform's server-side `addon_service` filter excludes
|
|
@@ -21,38 +23,8 @@ import type { AddOnOptions, ResolveAddonOptions, ResolvedAddOn, ResolvedAddOnAtt
|
|
|
21
23
|
* Errors:
|
|
22
24
|
* - throws `AddonNotFoundError` if no match is found
|
|
23
25
|
* - throws `AddonAmbiguousError` if multiple matches remain after filtering
|
|
24
|
-
*
|
|
25
|
-
* For attachment-based resolution (e.g. `DATABASE_URL` on a particular
|
|
26
|
-
* app), use `resolveAddonByAttachment` instead.
|
|
27
26
|
*/
|
|
28
27
|
export declare function resolveAddon(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, options?: ResolveAddonOptions): Promise<ResolvedAddOn>;
|
|
29
|
-
/**
|
|
30
|
-
* Describe an add-on *attachment* via its name on a given app.
|
|
31
|
-
*
|
|
32
|
-
* Use this when you have an attachment name (e.g. `DATABASE_URL`,
|
|
33
|
-
* `HEROKU_POSTGRESQL_GREEN`) on a known app and need the attachment
|
|
34
|
-
* itself — including its context-scoped `web_url` (the dashboard URL for
|
|
35
|
-
* opening the add-on *from this app*, which differs from the add-on's own
|
|
36
|
-
* billing-app `web_url`). Calls `addOnAttachment.resolution` and returns
|
|
37
|
-
* the matched attachment with its resolved add-on.
|
|
38
|
-
*
|
|
39
|
-
* `web_url` is `string | null` by design: it is `null` for add-ons that
|
|
40
|
-
* expose no web dashboard. A `null` here is a valid result, not a
|
|
41
|
-
* not-found — callers who need a URL must handle it (e.g. fall back to
|
|
42
|
-
* the add-on's own `web_url` via `resolveAddon`).
|
|
43
|
-
*
|
|
44
|
-
* To resolve just the add-on the attachment points to, use
|
|
45
|
-
* `resolveAddonByAttachment`. For resolving by add-on identity, use
|
|
46
|
-
* `resolveAddon`.
|
|
47
|
-
*/
|
|
48
|
-
export declare function describeAttachment(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, attachmentName: string, options?: AddOnOptions): Promise<ResolvedAddOnAttachment>;
|
|
49
|
-
/**
|
|
50
|
-
* Resolve the add-on that an attachment points to, by attachment name on a
|
|
51
|
-
* given app. A thin wrapper over `describeAttachment` that returns only the
|
|
52
|
-
* resolved add-on (not the attachment). Use `describeAttachment` when you
|
|
53
|
-
* need the attachment's context-scoped `web_url`.
|
|
54
|
-
*/
|
|
55
|
-
export declare function resolveAddonByAttachment(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, attachmentName: string, options?: AddOnOptions): Promise<ResolvedAddOn>;
|
|
56
28
|
export declare function resolveAddonInternal(platform: PlatformClient, addonIdentity: string, options?: {
|
|
57
29
|
addonService?: string;
|
|
58
30
|
appIdentity?: string;
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { NotFoundError } from '@heroku/heroku-fetch';
|
|
2
1
|
import { debug } from './debug.js';
|
|
3
2
|
import { AddonAmbiguousError, AddonNotFoundError } from './errors.js';
|
|
4
3
|
/**
|
|
5
4
|
* Resolve a Platform add-on by identity.
|
|
6
5
|
*
|
|
7
6
|
* The add-on identity may be:
|
|
8
|
-
* -
|
|
9
|
-
* - a globally-unique name (`postgres-curved-12345`)
|
|
10
|
-
* -
|
|
7
|
+
* - an add-on UUID (`d5e3f2a4-...`)
|
|
8
|
+
* - a globally-unique add-on name (`postgres-curved-12345`)
|
|
9
|
+
* - an add-on service slug (`heroku-postgresql`)
|
|
10
|
+
* - an add-on plan slug (`heroku-postgresql:essential-0`)
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* It may also be an add-on attachment identity:
|
|
13
|
+
* - an attachment UUID (`2a4d5e3f-...`), name (`DATABASE`), or config
|
|
14
|
+
* var (`DATABASE_URL`). These identities require `appIdentity`.
|
|
15
|
+
* - an attachment name or config var prefixed with the app name
|
|
16
|
+
* (`my-app::DATABASE`, `my-app::DATABASE_URL`).
|
|
16
17
|
*
|
|
17
18
|
* `addonService` (e.g. `heroku-postgresql`) is filtered client-side after
|
|
18
19
|
* the resolve. The platform's server-side `addon_service` filter excludes
|
|
@@ -21,92 +22,25 @@ import { AddonAmbiguousError, AddonNotFoundError } from './errors.js';
|
|
|
21
22
|
* Errors:
|
|
22
23
|
* - throws `AddonNotFoundError` if no match is found
|
|
23
24
|
* - throws `AddonAmbiguousError` if multiple matches remain after filtering
|
|
24
|
-
*
|
|
25
|
-
* For attachment-based resolution (e.g. `DATABASE_URL` on a particular
|
|
26
|
-
* app), use `resolveAddonByAttachment` instead.
|
|
27
25
|
*/
|
|
28
26
|
export async function resolveAddon(ctx, addonIdentity, options = {}) {
|
|
29
27
|
options.signal?.throwIfAborted();
|
|
30
28
|
return resolveAddonInternal(ctx.platform, addonIdentity, options);
|
|
31
29
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Describe an add-on *attachment* via its name on a given app.
|
|
34
|
-
*
|
|
35
|
-
* Use this when you have an attachment name (e.g. `DATABASE_URL`,
|
|
36
|
-
* `HEROKU_POSTGRESQL_GREEN`) on a known app and need the attachment
|
|
37
|
-
* itself — including its context-scoped `web_url` (the dashboard URL for
|
|
38
|
-
* opening the add-on *from this app*, which differs from the add-on's own
|
|
39
|
-
* billing-app `web_url`). Calls `addOnAttachment.resolution` and returns
|
|
40
|
-
* the matched attachment with its resolved add-on.
|
|
41
|
-
*
|
|
42
|
-
* `web_url` is `string | null` by design: it is `null` for add-ons that
|
|
43
|
-
* expose no web dashboard. A `null` here is a valid result, not a
|
|
44
|
-
* not-found — callers who need a URL must handle it (e.g. fall back to
|
|
45
|
-
* the add-on's own `web_url` via `resolveAddon`).
|
|
46
|
-
*
|
|
47
|
-
* To resolve just the add-on the attachment points to, use
|
|
48
|
-
* `resolveAddonByAttachment`. For resolving by add-on identity, use
|
|
49
|
-
* `resolveAddon`.
|
|
50
|
-
*/
|
|
51
|
-
export async function describeAttachment(ctx, appIdentity, attachmentName, options = {}) {
|
|
52
|
-
options.signal?.throwIfAborted();
|
|
53
|
-
debug('describeAttachment app=%s attachment=%s', appIdentity, attachmentName);
|
|
54
|
-
// Scope the in-flight resolution call to the caller's signal so an abort
|
|
55
|
-
// mid-request is honored, not just before it starts.
|
|
56
|
-
const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
|
|
57
|
-
const matches = await platform.addOnAttachment.resolution({
|
|
58
|
-
// eslint-disable-next-line camelcase
|
|
59
|
-
addon_attachment: attachmentName,
|
|
60
|
-
app: appIdentity,
|
|
61
|
-
});
|
|
62
|
-
const attachment = matches[0];
|
|
63
|
-
if (!attachment?.addon?.id || !attachment.addon.app?.id) {
|
|
64
|
-
debug('describeAttachment matches=%d (no usable add-on returned)', matches.length);
|
|
65
|
-
throw new AddonNotFoundError();
|
|
66
|
-
}
|
|
67
|
-
debug('describeAttachment resolved addon=%s app=%s', attachment.addon.id, attachment.addon.app.id);
|
|
68
|
-
return attachment;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Resolve the add-on that an attachment points to, by attachment name on a
|
|
72
|
-
* given app. A thin wrapper over `describeAttachment` that returns only the
|
|
73
|
-
* resolved add-on (not the attachment). Use `describeAttachment` when you
|
|
74
|
-
* need the attachment's context-scoped `web_url`.
|
|
75
|
-
*/
|
|
76
|
-
export async function resolveAddonByAttachment(ctx, appIdentity, attachmentName, options = {}) {
|
|
77
|
-
return (await describeAttachment(ctx, appIdentity, attachmentName, options)).addon;
|
|
78
|
-
}
|
|
79
30
|
export async function resolveAddonInternal(platform, addonIdentity, options = {}) {
|
|
80
31
|
const { addonService, appIdentity } = options;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (!appIdentity || addonIdentity.includes('::')) {
|
|
94
|
-
debug('resolve scope=global reason=%s', appIdentity ? 'namespaced-identity' : 'no-app');
|
|
95
|
-
return resolveBy();
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
return await resolveBy(appIdentity);
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
if (isAddOnNotFound(error)) {
|
|
102
|
-
debug('resolve app-scope 404 add_on, falling back to global addon=%s', addonIdentity);
|
|
103
|
-
return resolveBy();
|
|
104
|
-
}
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function isAddOnNotFound(error) {
|
|
109
|
-
return error instanceof NotFoundError && error.resource === 'add_on';
|
|
32
|
+
// If the add-on identity is app-scoped (contains `::`), we do not pass `appIdentity`
|
|
33
|
+
const app = appIdentity && !addonIdentity.includes('::') ? appIdentity : undefined;
|
|
34
|
+
const body = app ? { addon: addonIdentity, app } : { addon: addonIdentity };
|
|
35
|
+
debug('resolve addon=%s app=%s service=%s', addonIdentity, app ?? '<global>', addonService ?? '<any>');
|
|
36
|
+
const matches = await platform.addOn.resolution(body);
|
|
37
|
+
const filtered = addonService
|
|
38
|
+
? matches.filter(addon => addon.addon_service?.name === addonService)
|
|
39
|
+
: matches;
|
|
40
|
+
debug('resolve matches=%d filtered=%d (service=%s)', matches.length, filtered.length, addonService ?? '<any>');
|
|
41
|
+
const resolvedAddon = singularize(filtered);
|
|
42
|
+
debug('resolve resolved addon=%s app=%s', resolvedAddon.id, resolvedAddon.app.id);
|
|
43
|
+
return resolvedAddon;
|
|
110
44
|
}
|
|
111
45
|
function singularize(matches) {
|
|
112
46
|
if (matches.length === 0) {
|
|
@@ -7,10 +7,14 @@ export type ResolveAddonOptions = AddOnOptions & {
|
|
|
7
7
|
appIdentity?: string;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
* An add-on
|
|
11
|
-
*
|
|
12
|
-
* types
|
|
13
|
-
*
|
|
10
|
+
* An add-on that has passed through a resolver's `singularize` check:
|
|
11
|
+
* `app.id` and `id` are guaranteed non-null at runtime. At the currently
|
|
12
|
+
* installed `@heroku/types` schema, `AddOn` already types these as
|
|
13
|
+
* required, so this doesn't narrow anything today — it exists to give the
|
|
14
|
+
* SDK a stable name decoupled from the generated type (insurance against
|
|
15
|
+
* that schema loosening these back to optional) and to document, by name,
|
|
16
|
+
* that a value carrying this type has actually been through resolution
|
|
17
|
+
* rather than a plain `list()`/`info()` call.
|
|
14
18
|
*/
|
|
15
19
|
export type ResolvedAddOn = AddOn & {
|
|
16
20
|
app: AddOn['app'] & {
|
|
@@ -18,24 +22,6 @@ export type ResolvedAddOn = AddOn & {
|
|
|
18
22
|
};
|
|
19
23
|
id: string;
|
|
20
24
|
};
|
|
21
|
-
/**
|
|
22
|
-
* An attachment whose add-on `id`/`app.id` are guaranteed non-null (as the
|
|
23
|
-
* resolver enforces at runtime). `web_url` is scoped to the attached app's
|
|
24
|
-
* context — the dashboard URL for opening this add-on *from this app*, which
|
|
25
|
-
* differs from the add-on's own (billing-app) `web_url`.
|
|
26
|
-
*
|
|
27
|
-
* `web_url` remains `string | null` by design (inherited from
|
|
28
|
-
* `AddOnAttachment`): it is `null` for add-ons that expose no web dashboard.
|
|
29
|
-
* The resolver does not guarantee a URL — callers must handle the `null` case.
|
|
30
|
-
*/
|
|
31
|
-
export type ResolvedAddOnAttachment = AddOnAttachment & {
|
|
32
|
-
addon: NonNullable<AddOnAttachment['addon']> & {
|
|
33
|
-
app: {
|
|
34
|
-
id: string;
|
|
35
|
-
};
|
|
36
|
-
id: string;
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
25
|
export type DescribedAddOn = ResolvedAddOn & {
|
|
40
26
|
attachments: AddOnAttachment[];
|
|
41
27
|
};
|
|
@@ -5,9 +5,8 @@ import type { UpgradeAddOnOptions } from './types.js';
|
|
|
5
5
|
* Change the plan of an add-on.
|
|
6
6
|
*
|
|
7
7
|
* Resolves the add-on first so the caller can pass any identifier
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* falls back to a global resolve if the platform returns 404 add_on.
|
|
8
|
+
* `resolveAddon` accepts. When `appIdentity` is provided, the resolve
|
|
9
|
+
* is scoped to that app.
|
|
11
10
|
*
|
|
12
11
|
* If `plan` is unqualified (no `:`), it's prefixed with the resolved
|
|
13
12
|
* add-on's `addon_service.name` — so callers can pass `hobby` rather
|
|
@@ -4,9 +4,8 @@ import { resolveAddonInternal } from './resolve.js';
|
|
|
4
4
|
* Change the plan of an add-on.
|
|
5
5
|
*
|
|
6
6
|
* Resolves the add-on first so the caller can pass any identifier
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* falls back to a global resolve if the platform returns 404 add_on.
|
|
7
|
+
* `resolveAddon` accepts. When `appIdentity` is provided, the resolve
|
|
8
|
+
* is scoped to that app.
|
|
10
9
|
*
|
|
11
10
|
* If `plan` is unqualified (no `:`), it's prefixed with the resolved
|
|
12
11
|
* add-on's `addon_service.name` — so callers can pass `hobby` rather
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ResolveAddonAttachmentOptions } from './resolve.js';
|
|
2
|
+
export { AddonAttachmentAmbiguousError, AddonAttachmentNotFoundError, resolveAddonAttachment } from './resolve.js';
|
|
3
|
+
export type { ResolveAddonAttachmentOptions, ResolvedAddOnAttachment } from './resolve.js';
|
|
4
|
+
export declare const addOnAttachmentExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "addOnAttachment", {
|
|
5
|
+
resolve: (appIdentity: string | undefined, attachmentName: string, options?: ResolveAddonAttachmentOptions) => Promise<import("./resolve.js").ResolvedAddOnAttachment>;
|
|
6
|
+
}>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { resolveAddonAttachment } from './resolve.js';
|
|
3
|
+
export { AddonAttachmentAmbiguousError, AddonAttachmentNotFoundError, resolveAddonAttachment } from './resolve.js';
|
|
4
|
+
export const addOnAttachmentExtensions = extendResource('platform', 'addOnAttachment', ctx => ({
|
|
5
|
+
resolve: (appIdentity, attachmentName, options) => resolveAddonAttachment(ctx, appIdentity, attachmentName, options),
|
|
6
|
+
}));
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AddOnAttachment } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
export declare class AddonAttachmentNotFoundError extends Error {
|
|
4
|
+
readonly resource: string;
|
|
5
|
+
readonly id = "not_found";
|
|
6
|
+
readonly statusCode = 404;
|
|
7
|
+
constructor(resource?: string);
|
|
8
|
+
get body(): {
|
|
9
|
+
id: string;
|
|
10
|
+
message: string;
|
|
11
|
+
resource: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare class AddonAttachmentAmbiguousError extends Error {
|
|
15
|
+
readonly matches: AddOnAttachmentWithInclusions[];
|
|
16
|
+
readonly id = "multiple_matches";
|
|
17
|
+
readonly statusCode = 422;
|
|
18
|
+
constructor(matches: AddOnAttachmentWithInclusions[]);
|
|
19
|
+
get body(): {
|
|
20
|
+
id: string;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `Accept-Inclusion: addon:plan,config_vars` requests the add-on's plan and
|
|
26
|
+
* the attachment's config var names, but @heroku/types doesn't model either
|
|
27
|
+
* inclusion. Extend locally rather than casting through `any`.
|
|
28
|
+
*/
|
|
29
|
+
export type AddOnAttachmentWithInclusions = AddOnAttachment & {
|
|
30
|
+
addon: AddOnAttachment['addon'] & {
|
|
31
|
+
plan: {
|
|
32
|
+
id?: string;
|
|
33
|
+
name: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
config_vars: string[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* An attachment that has passed through `singularize`: its add-on `id`/
|
|
40
|
+
* `app.id` are guaranteed non-null at runtime. At the currently installed
|
|
41
|
+
* `@heroku/types` schema, `AddOnAttachment` already types these as
|
|
42
|
+
* required, so this doesn't narrow anything today — it exists to give the
|
|
43
|
+
* SDK a stable name decoupled from the generated type (insurance against
|
|
44
|
+
* that schema loosening these back to optional) and to document, by name,
|
|
45
|
+
* that a value carrying this type has actually been through resolution.
|
|
46
|
+
*/
|
|
47
|
+
export type ResolvedAddOnAttachment = AddOnAttachmentWithInclusions & {
|
|
48
|
+
addon: NonNullable<AddOnAttachmentWithInclusions['addon']> & {
|
|
49
|
+
app: {
|
|
50
|
+
id: string;
|
|
51
|
+
};
|
|
52
|
+
id: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export type ResolveAddonAttachmentOptions = {
|
|
56
|
+
/**
|
|
57
|
+
* Restrict matches to attachments whose add-on plan belongs to this
|
|
58
|
+
* add-on service (e.g. `heroku-postgresql`), filtered client-side.
|
|
59
|
+
*/
|
|
60
|
+
addonService?: string;
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a Platform add-on *attachment* by identity.
|
|
65
|
+
*
|
|
66
|
+
* The add-on attachment identity may be:
|
|
67
|
+
* - an attachment UUID (`d5e3f2a4-...`)
|
|
68
|
+
* - an attachment name (`DATABASE`)
|
|
69
|
+
* - a config var (`DATABASE_URL`)
|
|
70
|
+
* - an app-scoped attachment name or config var (`my-app::DATABASE`,
|
|
71
|
+
* `my-app::DATABASE_URL`)
|
|
72
|
+
* - a globally-unique add-on name (`postgres-curved-12345`)
|
|
73
|
+
*
|
|
74
|
+
* To resolve just the add-on the attachment points to, use `resolveAddon`.
|
|
75
|
+
*
|
|
76
|
+
* Always sends `Accept-Inclusion: addon:plan,config_vars` on the resolve
|
|
77
|
+
* request so matches come back with their add-on's plan (needed to filter
|
|
78
|
+
* by `addonService`) and their own config var names.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveAddonAttachment(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string | undefined, attachmentName: string, options?: ResolveAddonAttachmentOptions): Promise<ResolvedAddOnAttachment>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import createDebug from 'debug';
|
|
2
|
+
const debug = createDebug('heroku:sdk:resources:add-on-attachment');
|
|
3
|
+
export class AddonAttachmentNotFoundError extends Error {
|
|
4
|
+
resource;
|
|
5
|
+
id = 'not_found';
|
|
6
|
+
statusCode = 404;
|
|
7
|
+
constructor(resource = 'addon attachment') {
|
|
8
|
+
super(`Couldn't find that ${resource}.`);
|
|
9
|
+
this.resource = resource;
|
|
10
|
+
this.name = 'AddonAttachmentNotFoundError';
|
|
11
|
+
}
|
|
12
|
+
get body() {
|
|
13
|
+
return { id: this.id, message: this.message, resource: this.resource };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class AddonAttachmentAmbiguousError extends Error {
|
|
17
|
+
matches;
|
|
18
|
+
id = 'multiple_matches';
|
|
19
|
+
statusCode = 422;
|
|
20
|
+
constructor(matches) {
|
|
21
|
+
super(`Ambiguous identifier; multiple matching add-on attachments found: ${matches.map(m => m.name).join(', ')}.`);
|
|
22
|
+
this.matches = matches;
|
|
23
|
+
this.name = 'AddonAttachmentAmbiguousError';
|
|
24
|
+
}
|
|
25
|
+
get body() {
|
|
26
|
+
return { id: this.id, message: this.message };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a Platform add-on *attachment* by identity.
|
|
31
|
+
*
|
|
32
|
+
* The add-on attachment identity may be:
|
|
33
|
+
* - an attachment UUID (`d5e3f2a4-...`)
|
|
34
|
+
* - an attachment name (`DATABASE`)
|
|
35
|
+
* - a config var (`DATABASE_URL`)
|
|
36
|
+
* - an app-scoped attachment name or config var (`my-app::DATABASE`,
|
|
37
|
+
* `my-app::DATABASE_URL`)
|
|
38
|
+
* - a globally-unique add-on name (`postgres-curved-12345`)
|
|
39
|
+
*
|
|
40
|
+
* To resolve just the add-on the attachment points to, use `resolveAddon`.
|
|
41
|
+
*
|
|
42
|
+
* Always sends `Accept-Inclusion: addon:plan,config_vars` on the resolve
|
|
43
|
+
* request so matches come back with their add-on's plan (needed to filter
|
|
44
|
+
* by `addonService`) and their own config var names.
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveAddonAttachment(ctx, appIdentity, attachmentName, options = {}) {
|
|
47
|
+
const { addonService, signal } = options;
|
|
48
|
+
signal?.throwIfAborted();
|
|
49
|
+
debug('resolve app=%s attachment=%s service=%s', appIdentity ?? '<global>', attachmentName, addonService ?? '<any>');
|
|
50
|
+
const platform = ctx.platform
|
|
51
|
+
.withHeaders({ 'Accept-Inclusion': 'addon:plan,config_vars' })
|
|
52
|
+
.withOptions({ signal });
|
|
53
|
+
const matches = await platform.addOnAttachment.resolution({
|
|
54
|
+
// eslint-disable-next-line camelcase
|
|
55
|
+
addon_attachment: attachmentName,
|
|
56
|
+
app: appIdentity,
|
|
57
|
+
});
|
|
58
|
+
const filtered = addonService
|
|
59
|
+
? matches.filter(match => match.addon?.plan?.name?.split(':', 2)[0] === addonService)
|
|
60
|
+
: matches;
|
|
61
|
+
debug('resolve matches=%d filtered=%d (service=%s)', matches.length, filtered.length, addonService ?? '<any>');
|
|
62
|
+
const resolvedAttachment = singularize(filtered);
|
|
63
|
+
debug('resolve resolved attachment=%s addon=%s app=%s', resolvedAttachment.id, resolvedAttachment.addon.id, resolvedAttachment.addon.app.id);
|
|
64
|
+
return resolvedAttachment;
|
|
65
|
+
}
|
|
66
|
+
function singularize(matches) {
|
|
67
|
+
if (matches.length === 0) {
|
|
68
|
+
throw new AddonAttachmentNotFoundError();
|
|
69
|
+
}
|
|
70
|
+
if (matches.length > 1) {
|
|
71
|
+
throw new AddonAttachmentAmbiguousError(matches);
|
|
72
|
+
}
|
|
73
|
+
const match = matches[0];
|
|
74
|
+
if (!match.addon?.id || !match.addon?.app?.id) {
|
|
75
|
+
throw new Error(`Resolved attachment is missing required add-on fields (addon.id=${match.addon.id}, addon.app.id=${match.addon.app.id})`);
|
|
76
|
+
}
|
|
77
|
+
return match;
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroku/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Heroku SDK",
|
|
5
5
|
"author": "Heroku",
|
|
6
6
|
"type": "module",
|
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
"types": "./dist/resources/platform/add-on/index.d.ts",
|
|
51
51
|
"default": "./dist/resources/platform/add-on/index.js"
|
|
52
52
|
},
|
|
53
|
+
"./resources/platform/add-on-attachment": {
|
|
54
|
+
"types": "./dist/resources/platform/add-on-attachment/index.d.ts",
|
|
55
|
+
"default": "./dist/resources/platform/add-on-attachment/index.js"
|
|
56
|
+
},
|
|
53
57
|
"./resources/platform/app": {
|
|
54
58
|
"types": "./dist/resources/platform/app/index.d.ts",
|
|
55
59
|
"default": "./dist/resources/platform/app/index.js"
|