@heroku/sdk 0.1.0-alpha.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/LICENSE.txt +207 -0
- package/README.md +94 -0
- package/dist/compositions/add-on.d.ts +7 -0
- package/dist/compositions/add-on.js +6 -0
- package/dist/compositions/app.d.ts +8 -0
- package/dist/compositions/app.js +11 -0
- package/dist/compositions/dyno.d.ts +17 -0
- package/dist/compositions/dyno.js +23 -0
- package/dist/compositions/pg.d.ts +15 -0
- package/dist/compositions/pg.js +56 -0
- package/dist/compositions/pipeline.d.ts +33 -0
- package/dist/compositions/pipeline.js +146 -0
- package/dist/core/create-client.d.ts +32 -0
- package/dist/core/create-client.js +48 -0
- package/dist/core/dispatcher.d.ts +3 -0
- package/dist/core/dispatcher.js +80 -0
- package/dist/core/extend-resource.d.ts +24 -0
- package/dist/core/extend-resource.js +3 -0
- package/dist/core/extensions-proxy.d.ts +2 -0
- package/dist/core/extensions-proxy.js +56 -0
- package/dist/core/heroku-sdk.d.ts +14 -0
- package/dist/core/heroku-sdk.js +54 -0
- package/dist/core/interpolate-path.d.ts +3 -0
- package/dist/core/interpolate-path.js +13 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/resources/data/backup.d.ts +9 -0
- package/dist/resources/data/backup.js +10 -0
- package/dist/resources/data/database.d.ts +54 -0
- package/dist/resources/data/database.js +44 -0
- package/dist/resources/data/internal/resolve-addon-id.d.ts +2 -0
- package/dist/resources/data/internal/resolve-addon-id.js +13 -0
- package/dist/resources/data/internal/resolve-pg-database.d.ts +23 -0
- package/dist/resources/data/internal/resolve-pg-database.js +52 -0
- package/dist/resources/data/maintenance.d.ts +9 -0
- package/dist/resources/data/maintenance.js +10 -0
- package/dist/resources/data/postgres-credential.d.ts +11 -0
- package/dist/resources/data/postgres-credential.js +16 -0
- package/dist/resources/data/postgres-database.d.ts +32 -0
- package/dist/resources/data/postgres-database.js +14 -0
- package/dist/resources/data/postgres.d.ts +9 -0
- package/dist/resources/data/postgres.js +10 -0
- package/dist/resources/data/transfer-schedule.d.ts +9 -0
- package/dist/resources/data/transfer-schedule.js +10 -0
- package/dist/resources/data/transfer.d.ts +49 -0
- package/dist/resources/data/transfer.js +14 -0
- package/dist/resources/extensions/data.d.ts +4 -0
- package/dist/resources/extensions/data.js +4 -0
- package/dist/resources/extensions/platform.d.ts +8 -0
- package/dist/resources/extensions/platform.js +8 -0
- package/dist/resources/platform/add-on/create-and-wait.d.ts +28 -0
- package/dist/resources/platform/add-on/create-and-wait.js +63 -0
- package/dist/resources/platform/add-on/debug.d.ts +2 -0
- package/dist/resources/platform/add-on/debug.js +2 -0
- package/dist/resources/platform/add-on/describe.d.ts +20 -0
- package/dist/resources/platform/add-on/describe.js +51 -0
- package/dist/resources/platform/add-on/errors.d.ts +51 -0
- package/dist/resources/platform/add-on/errors.js +61 -0
- package/dist/resources/platform/add-on/index.d.ts +23 -0
- package/dist/resources/platform/add-on/index.js +25 -0
- package/dist/resources/platform/add-on/list-plans.d.ts +34 -0
- package/dist/resources/platform/add-on/list-plans.js +53 -0
- package/dist/resources/platform/add-on/pricing.d.ts +72 -0
- package/dist/resources/platform/add-on/pricing.js +100 -0
- package/dist/resources/platform/add-on/resolve.d.ts +43 -0
- package/dist/resources/platform/add-on/resolve.js +103 -0
- package/dist/resources/platform/add-on/types.d.ts +49 -0
- package/dist/resources/platform/add-on/types.js +1 -0
- package/dist/resources/platform/add-on/upgrade.d.ts +21 -0
- package/dist/resources/platform/add-on/upgrade.js +36 -0
- package/dist/resources/platform/add-on.d.ts +211 -0
- package/dist/resources/platform/add-on.js +354 -0
- package/dist/resources/platform/app/index.d.ts +58 -0
- package/dist/resources/platform/app/index.js +53 -0
- package/dist/resources/platform/app/info.d.ts +30 -0
- package/dist/resources/platform/app/info.js +27 -0
- package/dist/resources/platform/app.d.ts +56 -0
- package/dist/resources/platform/app.js +50 -0
- package/dist/resources/platform/dyno/index.d.ts +45 -0
- package/dist/resources/platform/dyno/index.js +26 -0
- package/dist/resources/platform/dyno.d.ts +45 -0
- package/dist/resources/platform/dyno.js +26 -0
- package/dist/resources/platform/log-session/index.d.ts +102 -0
- package/dist/resources/platform/log-session/index.js +223 -0
- package/dist/resources/platform/log-session/parser.d.ts +74 -0
- package/dist/resources/platform/log-session/parser.js +71 -0
- package/dist/resources/platform/log-session.d.ts +81 -0
- package/dist/resources/platform/log-session.js +167 -0
- package/dist/resources/platform/pipeline/coupling.d.ts +27 -0
- package/dist/resources/platform/pipeline/coupling.js +55 -0
- package/dist/resources/platform/pipeline/index.d.ts +53 -0
- package/dist/resources/platform/pipeline/index.js +75 -0
- package/dist/resources/platform/pipeline/promotion.d.ts +21 -0
- package/dist/resources/platform/pipeline/promotion.js +109 -0
- package/dist/resources/platform/pipeline-coupling.d.ts +27 -0
- package/dist/resources/platform/pipeline-coupling.js +55 -0
- package/dist/resources/platform/pipeline-promotion.d.ts +21 -0
- package/dist/resources/platform/pipeline-promotion.js +128 -0
- package/dist/resources/platform/pipeline.d.ts +53 -0
- package/dist/resources/platform/pipeline.js +75 -0
- package/dist/resources/platform/shield.d.ts +2 -0
- package/dist/resources/platform/shield.js +6 -0
- package/dist/services/data.d.ts +5 -0
- package/dist/services/data.js +5 -0
- package/dist/services/platform.d.ts +5 -0
- package/dist/services/platform.js +5 -0
- package/dist/utils/wait.d.ts +16 -0
- package/dist/utils/wait.js +35 -0
- package/package.json +72 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 30-day month assumption used to convert monthly pricing to an
|
|
3
|
+
* effective hourly rate. Heroku's documented dyno/add-on pricing
|
|
4
|
+
* uses this convention, so consumers displaying "max $X/month
|
|
5
|
+
* (~$Y/hour)" should compute hourly the same way.
|
|
6
|
+
*/
|
|
7
|
+
const HOURS_PER_MONTH = 24 * 30;
|
|
8
|
+
/**
|
|
9
|
+
* Break a plan's price down into monthly- and hourly-equivalent
|
|
10
|
+
* cents, normalizing across the API's billing unit. Consumers that
|
|
11
|
+
* display "max $X/month (~$Y/hour)" labels should call this rather
|
|
12
|
+
* than dividing `cents` by hours themselves; centralizing the math
|
|
13
|
+
* keeps the 30-day-month assumption (Heroku's documented convention)
|
|
14
|
+
* in one place.
|
|
15
|
+
*
|
|
16
|
+
* Returns `undefined` if the plan has no usable `price.cents` value.
|
|
17
|
+
* `Plan.price` is typed as optional in `@heroku/types`, and consumers
|
|
18
|
+
* occasionally receive partial responses depending on the Accept
|
|
19
|
+
* variant.
|
|
20
|
+
*
|
|
21
|
+
* `perMonthCents` and `perHourCents` are themselves `undefined` when
|
|
22
|
+
* the API reports a unit other than `'month'` or `'hour'` — callers
|
|
23
|
+
* should omit those parts of their label rather than show fabricated
|
|
24
|
+
* numbers.
|
|
25
|
+
*
|
|
26
|
+
* @param plan The plan to break down. Pass the whole `Plan` rather
|
|
27
|
+
* than just `plan.price` so the helper can pick up future fields
|
|
28
|
+
* without API churn at call sites.
|
|
29
|
+
*/
|
|
30
|
+
export function priceForPlan(plan) {
|
|
31
|
+
const { price } = plan;
|
|
32
|
+
if (!price || typeof price.cents !== 'number')
|
|
33
|
+
return undefined;
|
|
34
|
+
const { cents } = price;
|
|
35
|
+
const unit = price.unit ?? '';
|
|
36
|
+
let perMonthCents;
|
|
37
|
+
let perHourCents;
|
|
38
|
+
switch (unit) {
|
|
39
|
+
case 'hour': {
|
|
40
|
+
perHourCents = cents;
|
|
41
|
+
perMonthCents = cents * HOURS_PER_MONTH;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'month': {
|
|
45
|
+
perMonthCents = cents;
|
|
46
|
+
perHourCents = cents / HOURS_PER_MONTH;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
default: {
|
|
50
|
+
// Unknown billing unit — leave per-month / per-hour undefined
|
|
51
|
+
// so callers can omit those labels instead of rendering
|
|
52
|
+
// fabricated numbers.
|
|
53
|
+
perMonthCents = undefined;
|
|
54
|
+
perHourCents = undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
cents,
|
|
59
|
+
contract: Boolean(price.contract),
|
|
60
|
+
metered: Boolean(price.metered),
|
|
61
|
+
perHourCents,
|
|
62
|
+
perMonthCents,
|
|
63
|
+
unit,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Format a plan's price as a human-readable suffix:
|
|
68
|
+
* `"$0.007 / hour (Max $5/month)"`. Returns an empty string when the
|
|
69
|
+
* plan has no displayable price (no `price.cents`, an unknown unit,
|
|
70
|
+
* a metered plan, or a contract-priced plan) — callers can compose
|
|
71
|
+
* their final label as `${name}${suffix ? ' - ' + suffix : ''}`
|
|
72
|
+
* without per-call defensive checks.
|
|
73
|
+
*
|
|
74
|
+
* Locale and currency default to `'en-US'` / `'USD'`. Pass options
|
|
75
|
+
* to override.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const suffix = formatPlanPriceLabel(plan);
|
|
80
|
+
* const label = suffix ? `${plan.human_name} - ${suffix}` : plan.human_name;
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function formatPlanPriceLabel(plan, options = {}) {
|
|
84
|
+
const breakdown = priceForPlan(plan);
|
|
85
|
+
if (!breakdown)
|
|
86
|
+
return '';
|
|
87
|
+
// Metered and contract-priced plans don't have a meaningful flat
|
|
88
|
+
// monthly/hourly label — the consumer should describe them
|
|
89
|
+
// differently (e.g. "metered", "contract pricing"). Returning an
|
|
90
|
+
// empty string here lets the caller fall back to the plan name.
|
|
91
|
+
if (breakdown.metered || breakdown.contract)
|
|
92
|
+
return '';
|
|
93
|
+
if (breakdown.perMonthCents === undefined || breakdown.perHourCents === undefined)
|
|
94
|
+
return '';
|
|
95
|
+
const { currency = 'USD', locale = 'en-US' } = options;
|
|
96
|
+
const formatter = new Intl.NumberFormat(locale, { currency, style: 'currency' });
|
|
97
|
+
const perHour = formatter.format(breakdown.perHourCents / 100);
|
|
98
|
+
const perMonth = formatter.format(breakdown.perMonthCents / 100);
|
|
99
|
+
return `${perHour} / hour (Max ${perMonth}/month)`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
2
|
+
import type { PlatformClient } from '../../../services/platform.js';
|
|
3
|
+
import type { AddOnOptions, ResolveAddonOptions, ResolvedAddOn } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a Platform add-on by identity.
|
|
6
|
+
*
|
|
7
|
+
* The add-on identity may be:
|
|
8
|
+
* - a UUID (`d5e3f2a4-...`)
|
|
9
|
+
* - a globally-unique name (`postgres-curved-12345`)
|
|
10
|
+
* - a namespaced credential reference (`postgres-curved-12345::SECONDARY`)
|
|
11
|
+
*
|
|
12
|
+
* When `appIdentity` is provided, the resolve is scoped to that app first
|
|
13
|
+
* and falls back to a global resolve if the platform returns 404 add_on.
|
|
14
|
+
* Namespaced identities (containing `::`) skip the app-scoped lookup
|
|
15
|
+
* because they are globally unique.
|
|
16
|
+
*
|
|
17
|
+
* `addonService` (e.g. `heroku-postgresql`) is filtered client-side after
|
|
18
|
+
* the resolve. The platform's server-side `addon_service` filter excludes
|
|
19
|
+
* alpha add-ons, so we don't pass it through.
|
|
20
|
+
*
|
|
21
|
+
* Errors:
|
|
22
|
+
* - throws `AddonNotFoundError` if no match is found
|
|
23
|
+
* - 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
|
+
*/
|
|
28
|
+
export declare function resolveAddon(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, options?: ResolveAddonOptions): Promise<ResolvedAddOn>;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a Platform add-on via one of its attachments 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, rather than an add-on
|
|
34
|
+
* identity. Calls `addOnAttachment.resolution` and returns the add-on
|
|
35
|
+
* the matched attachment points to.
|
|
36
|
+
*
|
|
37
|
+
* For resolving by add-on identity, use `resolveAddon`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveAddonByAttachment(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, attachmentName: string, options?: AddOnOptions): Promise<ResolvedAddOn>;
|
|
40
|
+
export declare function resolveAddonInternal(platform: PlatformClient, addonIdentity: string, options?: {
|
|
41
|
+
addonService?: string;
|
|
42
|
+
appIdentity?: string;
|
|
43
|
+
}): Promise<ResolvedAddOn>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { NotFoundError } from '@heroku/heroku-fetch';
|
|
2
|
+
import { debug } from './debug.js';
|
|
3
|
+
import { AddonAmbiguousError, AddonNotFoundError } from './errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a Platform add-on by identity.
|
|
6
|
+
*
|
|
7
|
+
* The add-on identity may be:
|
|
8
|
+
* - a UUID (`d5e3f2a4-...`)
|
|
9
|
+
* - a globally-unique name (`postgres-curved-12345`)
|
|
10
|
+
* - a namespaced credential reference (`postgres-curved-12345::SECONDARY`)
|
|
11
|
+
*
|
|
12
|
+
* When `appIdentity` is provided, the resolve is scoped to that app first
|
|
13
|
+
* and falls back to a global resolve if the platform returns 404 add_on.
|
|
14
|
+
* Namespaced identities (containing `::`) skip the app-scoped lookup
|
|
15
|
+
* because they are globally unique.
|
|
16
|
+
*
|
|
17
|
+
* `addonService` (e.g. `heroku-postgresql`) is filtered client-side after
|
|
18
|
+
* the resolve. The platform's server-side `addon_service` filter excludes
|
|
19
|
+
* alpha add-ons, so we don't pass it through.
|
|
20
|
+
*
|
|
21
|
+
* Errors:
|
|
22
|
+
* - throws `AddonNotFoundError` if no match is found
|
|
23
|
+
* - 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
|
+
*/
|
|
28
|
+
export async function resolveAddon(ctx, addonIdentity, options = {}) {
|
|
29
|
+
options.signal?.throwIfAborted();
|
|
30
|
+
return resolveAddonInternal(ctx.platform, addonIdentity, options);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a Platform add-on via one of its attachments 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, rather than an add-on
|
|
37
|
+
* identity. Calls `addOnAttachment.resolution` and returns the add-on
|
|
38
|
+
* the matched attachment points to.
|
|
39
|
+
*
|
|
40
|
+
* For resolving by add-on identity, use `resolveAddon`.
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveAddonByAttachment(ctx, appIdentity, attachmentName, options = {}) {
|
|
43
|
+
options.signal?.throwIfAborted();
|
|
44
|
+
debug('resolveByAttachment app=%s attachment=%s', appIdentity, attachmentName);
|
|
45
|
+
const matches = await ctx.platform.addOnAttachment.resolution({
|
|
46
|
+
// eslint-disable-next-line camelcase
|
|
47
|
+
addon_attachment: attachmentName,
|
|
48
|
+
app: appIdentity,
|
|
49
|
+
});
|
|
50
|
+
const attachment = matches[0];
|
|
51
|
+
const addon = attachment?.addon;
|
|
52
|
+
if (!addon?.id || !addon.app?.id) {
|
|
53
|
+
debug('resolveByAttachment matches=%d (no usable add-on returned)', matches.length);
|
|
54
|
+
throw new AddonNotFoundError();
|
|
55
|
+
}
|
|
56
|
+
debug('resolveByAttachment resolved addon=%s app=%s', addon.id, addon.app.id);
|
|
57
|
+
return addon;
|
|
58
|
+
}
|
|
59
|
+
export async function resolveAddonInternal(platform, addonIdentity, options = {}) {
|
|
60
|
+
const { addonService, appIdentity } = options;
|
|
61
|
+
const resolveBy = async (app) => {
|
|
62
|
+
const body = app ? { addon: addonIdentity, app } : { addon: addonIdentity };
|
|
63
|
+
debug('resolve addon=%s app=%s service=%s', addonIdentity, app ?? '<global>', addonService ?? '<any>');
|
|
64
|
+
const matches = await platform.addOn.resolution(body);
|
|
65
|
+
const filtered = addonService
|
|
66
|
+
? matches.filter(addon => addon.addon_service?.name === addonService)
|
|
67
|
+
: matches;
|
|
68
|
+
debug('resolve matches=%d filtered=%d (service=%s)', matches.length, filtered.length, addonService ?? '<any>');
|
|
69
|
+
const resolvedAddon = singularize(filtered);
|
|
70
|
+
debug('resolve resolved addon=%s app=%s', resolvedAddon.id, resolvedAddon.app.id);
|
|
71
|
+
return resolvedAddon;
|
|
72
|
+
};
|
|
73
|
+
if (!appIdentity || addonIdentity.includes('::')) {
|
|
74
|
+
debug('resolve scope=global reason=%s', appIdentity ? 'namespaced-identity' : 'no-app');
|
|
75
|
+
return resolveBy();
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return await resolveBy(appIdentity);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (isAddOnNotFound(error)) {
|
|
82
|
+
debug('resolve app-scope 404 add_on, falling back to global addon=%s', addonIdentity);
|
|
83
|
+
return resolveBy();
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isAddOnNotFound(error) {
|
|
89
|
+
return error instanceof NotFoundError && error.resource === 'add_on';
|
|
90
|
+
}
|
|
91
|
+
function singularize(matches) {
|
|
92
|
+
if (matches.length === 0) {
|
|
93
|
+
throw new AddonNotFoundError();
|
|
94
|
+
}
|
|
95
|
+
if (matches.length > 1) {
|
|
96
|
+
throw new AddonAmbiguousError(matches);
|
|
97
|
+
}
|
|
98
|
+
const match = matches[0];
|
|
99
|
+
if (!match.id || !match.app?.id) {
|
|
100
|
+
throw new Error(`Resolved add-on is missing required fields (id=${match.id}, app.id=${match.app?.id})`);
|
|
101
|
+
}
|
|
102
|
+
return match;
|
|
103
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AddOn, AddOnAttachment } from '@heroku/types/3.sdk';
|
|
2
|
+
export type AddOnOptions = {
|
|
3
|
+
signal?: AbortSignal;
|
|
4
|
+
};
|
|
5
|
+
export type ResolveAddonOptions = AddOnOptions & {
|
|
6
|
+
addonService?: string;
|
|
7
|
+
appIdentity?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* An add-on whose `app.id` and `id` are guaranteed non-null. This is what
|
|
11
|
+
* the Platform's resolver returns for a successful match, but the schema
|
|
12
|
+
* types both as optional. `singularize` enforces it at runtime so callers
|
|
13
|
+
* can rely on the narrower type.
|
|
14
|
+
*/
|
|
15
|
+
export type ResolvedAddOn = AddOn & {
|
|
16
|
+
app: AddOn['app'] & {
|
|
17
|
+
id: string;
|
|
18
|
+
};
|
|
19
|
+
id: string;
|
|
20
|
+
};
|
|
21
|
+
export type DescribedAddOn = ResolvedAddOn & {
|
|
22
|
+
attachments: AddOnAttachment[];
|
|
23
|
+
};
|
|
24
|
+
export type UpgradeAddOnOptions = ResolveAddonOptions & {
|
|
25
|
+
onResolved?: (addon: ResolvedAddOn) => Promise<void> | void;
|
|
26
|
+
};
|
|
27
|
+
export type CreateAndWaitOptions = {
|
|
28
|
+
/**
|
|
29
|
+
* Fires once after the initial create returns and the add-on is
|
|
30
|
+
* still provisioning, before the first poll. Receives the create
|
|
31
|
+
* response. Useful for surfacing two-phase UX: e.g. "Creating
|
|
32
|
+
* <plan>... <price>" followed by "Waiting for <addonName>...".
|
|
33
|
+
*
|
|
34
|
+
* Only invoked when `wait: true` and the create response state is
|
|
35
|
+
* `provisioning`.
|
|
36
|
+
*/
|
|
37
|
+
onProvisioning?: (addon: AddOn) => Promise<void> | void;
|
|
38
|
+
/**
|
|
39
|
+
* If true, poll until the add-on leaves the `provisioning` state. If
|
|
40
|
+
* the final state is anything other than `provisioned`/`provisioning`
|
|
41
|
+
* (e.g. `deprovisioned`), throws `AddonProvisioningFailedError`.
|
|
42
|
+
*
|
|
43
|
+
* If false (the default), returns immediately after the create call —
|
|
44
|
+
* even if the add-on is still provisioning.
|
|
45
|
+
*/
|
|
46
|
+
wait?: boolean;
|
|
47
|
+
/** Polling interval in milliseconds. Defaults to 5000. */
|
|
48
|
+
waitIntervalMs?: number;
|
|
49
|
+
} & AddOnOptions;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AddOn } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
import type { UpgradeAddOnOptions } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Change the plan of an add-on.
|
|
6
|
+
*
|
|
7
|
+
* Resolves the add-on first so the caller can pass any identifier
|
|
8
|
+
* (UUID, globally unique name, or namespaced `service::name`). When
|
|
9
|
+
* `appIdentity` is provided, the resolve is scoped to that app and
|
|
10
|
+
* falls back to a global resolve if the platform returns 404 add_on.
|
|
11
|
+
*
|
|
12
|
+
* If `plan` is unqualified (no `:`), it's prefixed with the resolved
|
|
13
|
+
* add-on's `addon_service.name` — so callers can pass `hobby` rather
|
|
14
|
+
* than `heroku-redis:hobby` if they don't already know the service.
|
|
15
|
+
*
|
|
16
|
+
* `options.onResolved` fires after the resolve and before the update,
|
|
17
|
+
* receiving the resolved add-on. Useful for surfacing pre-update
|
|
18
|
+
* state (the add-on's current plan, app, etc.) without an extra
|
|
19
|
+
* round-trip.
|
|
20
|
+
*/
|
|
21
|
+
export declare function upgrade(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, plan: string, options?: UpgradeAddOnOptions): Promise<AddOn>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { debug } from './debug.js';
|
|
2
|
+
import { resolveAddonInternal } from './resolve.js';
|
|
3
|
+
/**
|
|
4
|
+
* Change the plan of an add-on.
|
|
5
|
+
*
|
|
6
|
+
* Resolves the add-on first so the caller can pass any identifier
|
|
7
|
+
* (UUID, globally unique name, or namespaced `service::name`). When
|
|
8
|
+
* `appIdentity` is provided, the resolve is scoped to that app and
|
|
9
|
+
* falls back to a global resolve if the platform returns 404 add_on.
|
|
10
|
+
*
|
|
11
|
+
* If `plan` is unqualified (no `:`), it's prefixed with the resolved
|
|
12
|
+
* add-on's `addon_service.name` — so callers can pass `hobby` rather
|
|
13
|
+
* than `heroku-redis:hobby` if they don't already know the service.
|
|
14
|
+
*
|
|
15
|
+
* `options.onResolved` fires after the resolve and before the update,
|
|
16
|
+
* receiving the resolved add-on. Useful for surfacing pre-update
|
|
17
|
+
* state (the add-on's current plan, app, etc.) without an extra
|
|
18
|
+
* round-trip.
|
|
19
|
+
*/
|
|
20
|
+
export async function upgrade(ctx, addonIdentity, plan, options = {}) {
|
|
21
|
+
options.signal?.throwIfAborted();
|
|
22
|
+
const addon = await resolveAddonInternal(ctx.platform, addonIdentity, {
|
|
23
|
+
addonService: options.addonService,
|
|
24
|
+
appIdentity: options.appIdentity,
|
|
25
|
+
});
|
|
26
|
+
await options.onResolved?.(addon);
|
|
27
|
+
options.signal?.throwIfAborted();
|
|
28
|
+
const qualifiedPlan = plan.includes(':')
|
|
29
|
+
? plan
|
|
30
|
+
: `${addon.addon_service?.name}:${plan}`;
|
|
31
|
+
if (qualifiedPlan !== plan) {
|
|
32
|
+
debug('upgrade plan qualified plan=%s qualified=%s', plan, qualifiedPlan);
|
|
33
|
+
}
|
|
34
|
+
debug('upgrade addon=%s app=%s plan=%s', addon.id, addon.app.id, qualifiedPlan);
|
|
35
|
+
return ctx.platform.addOn.update(addon.app.id, addon.id, { plan: qualifiedPlan });
|
|
36
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { AddOn, AddOnAttachment, AddOnCreateOpts, Plan } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
3
|
+
export type AddOnOptions = {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
};
|
|
6
|
+
export type ResolveAddonOptions = AddOnOptions & {
|
|
7
|
+
addonService?: string;
|
|
8
|
+
appIdentity?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* An add-on whose `app.id` and `id` are guaranteed non-null. This is what
|
|
12
|
+
* the Platform's resolver returns for a successful match, but the schema
|
|
13
|
+
* types both as optional. `singularize` enforces it at runtime so callers
|
|
14
|
+
* can rely on the narrower type.
|
|
15
|
+
*/
|
|
16
|
+
export type ResolvedAddOn = AddOn & {
|
|
17
|
+
app: AddOn['app'] & {
|
|
18
|
+
id: string;
|
|
19
|
+
};
|
|
20
|
+
id: string;
|
|
21
|
+
};
|
|
22
|
+
export type DescribedAddOn = ResolvedAddOn & {
|
|
23
|
+
attachments: AddOnAttachment[];
|
|
24
|
+
};
|
|
25
|
+
export type UpgradeAddOnOptions = ResolveAddonOptions & {
|
|
26
|
+
onResolved?: (addon: ResolvedAddOn) => Promise<void> | void;
|
|
27
|
+
};
|
|
28
|
+
export declare class AddonNotFoundError extends Error {
|
|
29
|
+
readonly resource: string;
|
|
30
|
+
readonly id = "not_found";
|
|
31
|
+
readonly statusCode = 404;
|
|
32
|
+
constructor(resource?: string);
|
|
33
|
+
get body(): {
|
|
34
|
+
id: string;
|
|
35
|
+
message: string;
|
|
36
|
+
resource: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export declare class AddonAmbiguousError extends Error {
|
|
40
|
+
readonly matches: AddOn[];
|
|
41
|
+
readonly id = "multiple_matches";
|
|
42
|
+
readonly statusCode = 422;
|
|
43
|
+
constructor(matches: AddOn[]);
|
|
44
|
+
get body(): {
|
|
45
|
+
id: string;
|
|
46
|
+
message: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Thrown by `createAndWait` when the Platform returns 423
|
|
51
|
+
* `confirmation_required`. Callers that want to prompt the user should
|
|
52
|
+
* catch this, gather a confirmation value, and retry the call with
|
|
53
|
+
* `body.confirm` set.
|
|
54
|
+
*/
|
|
55
|
+
export declare class AddonConfirmationRequiredError extends Error {
|
|
56
|
+
readonly platformMessage: string;
|
|
57
|
+
readonly id = "confirmation_required";
|
|
58
|
+
readonly statusCode = 423;
|
|
59
|
+
constructor(platformMessage: string);
|
|
60
|
+
get body(): {
|
|
61
|
+
id: string;
|
|
62
|
+
message: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Thrown by `createAndWait` when the Platform reports the add-on
|
|
67
|
+
* settled into a non-provisioned terminal state (e.g. `deprovisioned`).
|
|
68
|
+
*/
|
|
69
|
+
export declare class AddonProvisioningFailedError extends Error {
|
|
70
|
+
readonly addon: AddOn;
|
|
71
|
+
readonly id = "provisioning_failed";
|
|
72
|
+
constructor(addon: AddOn);
|
|
73
|
+
get body(): {
|
|
74
|
+
id: string;
|
|
75
|
+
message: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Change the plan of an add-on.
|
|
80
|
+
*
|
|
81
|
+
* Resolves the add-on first so the caller can pass any identifier
|
|
82
|
+
* (UUID, globally unique name, or namespaced `service::name`). When
|
|
83
|
+
* `appIdentity` is provided, the resolve is scoped to that app and
|
|
84
|
+
* falls back to a global resolve if the platform returns 404 add_on.
|
|
85
|
+
*
|
|
86
|
+
* If `plan` is unqualified (no `:`), it's prefixed with the resolved
|
|
87
|
+
* add-on's `addon_service.name` — so callers can pass `hobby` rather
|
|
88
|
+
* than `heroku-redis:hobby` if they don't already know the service.
|
|
89
|
+
*
|
|
90
|
+
* `options.onResolved` fires after the resolve and before the update,
|
|
91
|
+
* receiving the resolved add-on. Useful for surfacing pre-update
|
|
92
|
+
* state (the add-on's current plan, app, etc.) without an extra
|
|
93
|
+
* round-trip.
|
|
94
|
+
*/
|
|
95
|
+
export declare function upgrade(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, plan: string, options?: UpgradeAddOnOptions): Promise<AddOn>;
|
|
96
|
+
export type CreateAndWaitOptions = {
|
|
97
|
+
/**
|
|
98
|
+
* Fires once after the initial create returns and the add-on is
|
|
99
|
+
* still provisioning, before the first poll. Receives the create
|
|
100
|
+
* response. Useful for surfacing two-phase UX: e.g. "Creating
|
|
101
|
+
* <plan>... <price>" followed by "Waiting for <addonName>...".
|
|
102
|
+
*
|
|
103
|
+
* Only invoked when `wait: true` and the create response state is
|
|
104
|
+
* `provisioning`.
|
|
105
|
+
*/
|
|
106
|
+
onProvisioning?: (addon: AddOn) => Promise<void> | void;
|
|
107
|
+
/**
|
|
108
|
+
* If true, poll until the add-on leaves the `provisioning` state. If
|
|
109
|
+
* the final state is anything other than `provisioned`/`provisioning`
|
|
110
|
+
* (e.g. `deprovisioned`), throws `AddonProvisioningFailedError`.
|
|
111
|
+
*
|
|
112
|
+
* If false (the default), returns immediately after the create call —
|
|
113
|
+
* even if the add-on is still provisioning.
|
|
114
|
+
*/
|
|
115
|
+
wait?: boolean;
|
|
116
|
+
/** Polling interval in milliseconds. Defaults to 5000. */
|
|
117
|
+
waitIntervalMs?: number;
|
|
118
|
+
} & AddOnOptions;
|
|
119
|
+
/**
|
|
120
|
+
* Create an add-on and optionally wait for provisioning to complete.
|
|
121
|
+
*
|
|
122
|
+
* Wraps `addOn.create` with three pieces of orchestration:
|
|
123
|
+
*
|
|
124
|
+
* - 423 `confirmation_required` from the platform is converted to a
|
|
125
|
+
* typed `AddonConfirmationRequiredError`. Callers should catch
|
|
126
|
+
* this, prompt the user for confirmation, and retry the call with
|
|
127
|
+
* `body.confirm` set.
|
|
128
|
+
* - When `wait: true` is set, polls `addOn.infoByApp` on a
|
|
129
|
+
* `waitIntervalMs` cadence until the add-on's `state` is no longer
|
|
130
|
+
* `provisioning`. Throws `AddonProvisioningFailedError` if the
|
|
131
|
+
* terminal state is `deprovisioned`.
|
|
132
|
+
* - `options.onProvisioning` fires once after the create response
|
|
133
|
+
* when polling is about to begin, letting callers surface a
|
|
134
|
+
* two-phase status display ("Created" → "Waiting").
|
|
135
|
+
*
|
|
136
|
+
* Both the create and poll calls are issued with `Accept-Expansion:
|
|
137
|
+
* addon_service,plan` so the response includes the full plan shape
|
|
138
|
+
* (price, unit, etc.). The create call additionally sets
|
|
139
|
+
* `X-Heroku-Legacy-Provider-Messages: true` to opt into
|
|
140
|
+
* provider-supplied `provision_message` text on the response — without
|
|
141
|
+
* it the platform silently omits that field.
|
|
142
|
+
*/
|
|
143
|
+
export declare function createAndWait(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, body: AddOnCreateOpts, options?: CreateAndWaitOptions): Promise<AddOn>;
|
|
144
|
+
/**
|
|
145
|
+
* List the plans available for an add-on service, sorted ascending by
|
|
146
|
+
* `price.cents`. Plans without a price (or with equal prices) are
|
|
147
|
+
* returned later in the list.
|
|
148
|
+
*/
|
|
149
|
+
export declare function listPlans(ctx: Pick<ResourceCtx, 'platform'>, serviceIdentity: string, options?: AddOnOptions): Promise<Plan[]>;
|
|
150
|
+
/**
|
|
151
|
+
* Resolve, describe, and decorate an add-on for display.
|
|
152
|
+
*
|
|
153
|
+
* - Resolves the add-on by identity (optionally scoped to an app).
|
|
154
|
+
* - Loads the add-on's attachments.
|
|
155
|
+
* - Returns a copy with `plan.price` set from `billed_price` (so the
|
|
156
|
+
* value reflects any grandfathered/contract pricing).
|
|
157
|
+
*
|
|
158
|
+
* If `appIdentity` is provided and the platform returns 404 (resource
|
|
159
|
+
* `add_on`), the resolve falls back to a global lookup. This handles
|
|
160
|
+
* the case where the add-on belongs to a different app than the one
|
|
161
|
+
* supplied.
|
|
162
|
+
*
|
|
163
|
+
* Requests the `version=3.sdk` accept variant with `addon_service,plan`
|
|
164
|
+
* expansion so the resolved add-on includes the full `Plan` shape
|
|
165
|
+
* (`price.unit`, etc.) needed to render pricing.
|
|
166
|
+
*/
|
|
167
|
+
export declare function describeAddon(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, options?: ResolveAddonOptions): Promise<DescribedAddOn>;
|
|
168
|
+
/**
|
|
169
|
+
* Resolve a Platform add-on by identity.
|
|
170
|
+
*
|
|
171
|
+
* The add-on identity may be:
|
|
172
|
+
* - a UUID (`d5e3f2a4-...`)
|
|
173
|
+
* - a globally-unique name (`postgres-curved-12345`)
|
|
174
|
+
* - a namespaced credential reference (`postgres-curved-12345::SECONDARY`)
|
|
175
|
+
*
|
|
176
|
+
* When `appIdentity` is provided, the resolve is scoped to that app first
|
|
177
|
+
* and falls back to a global resolve if the platform returns 404 add_on.
|
|
178
|
+
* Namespaced identities (containing `::`) skip the app-scoped lookup
|
|
179
|
+
* because they are globally unique.
|
|
180
|
+
*
|
|
181
|
+
* `addonService` (e.g. `heroku-postgresql`) is filtered client-side after
|
|
182
|
+
* the resolve. The platform's server-side `addon_service` filter excludes
|
|
183
|
+
* alpha add-ons, so we don't pass it through.
|
|
184
|
+
*
|
|
185
|
+
* Errors:
|
|
186
|
+
* - throws `AddonNotFoundError` if no match is found
|
|
187
|
+
* - throws `AddonAmbiguousError` if multiple matches remain after filtering
|
|
188
|
+
*
|
|
189
|
+
* For attachment-based resolution (e.g. `DATABASE_URL` on a particular
|
|
190
|
+
* app), use `resolveAddonByAttachment` instead.
|
|
191
|
+
*/
|
|
192
|
+
export declare function resolveAddon(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, options?: ResolveAddonOptions): Promise<ResolvedAddOn>;
|
|
193
|
+
/**
|
|
194
|
+
* Resolve a Platform add-on via one of its attachments on a given app.
|
|
195
|
+
*
|
|
196
|
+
* Use this when you have an attachment name (e.g. `DATABASE_URL`,
|
|
197
|
+
* `HEROKU_POSTGRESQL_GREEN`) on a known app, rather than an add-on
|
|
198
|
+
* identity. Calls `addOnAttachment.resolution` and returns the add-on
|
|
199
|
+
* the matched attachment points to.
|
|
200
|
+
*
|
|
201
|
+
* For resolving by add-on identity, use `resolveAddon`.
|
|
202
|
+
*/
|
|
203
|
+
export declare function resolveAddonByAttachment(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, attachmentName: string, options?: AddOnOptions): Promise<ResolvedAddOn>;
|
|
204
|
+
export declare const addOnExtensions: import("../../index.js").ResourceExtension<"platform", "addOn", {
|
|
205
|
+
createAndWait: (appIdentity: string, body: AddOnCreateOpts, options?: CreateAndWaitOptions) => Promise<AddOn>;
|
|
206
|
+
describe: (addonIdentity: string, options?: ResolveAddonOptions) => Promise<DescribedAddOn>;
|
|
207
|
+
listPlans: (serviceIdentity: string, options?: AddOnOptions) => Promise<Plan[]>;
|
|
208
|
+
resolve: (addonIdentity: string, options?: ResolveAddonOptions) => Promise<ResolvedAddOn>;
|
|
209
|
+
resolveByAttachment: (appIdentity: string, attachmentName: string, options?: AddOnOptions) => Promise<ResolvedAddOn>;
|
|
210
|
+
upgrade: (addonIdentity: string, plan: string, options?: UpgradeAddOnOptions) => Promise<AddOn>;
|
|
211
|
+
}>;
|