@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,9 @@
|
|
|
1
|
+
import type { PostgresInfoResult } from '@heroku/types/data';
|
|
2
|
+
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
3
|
+
export type PostgresOptions = {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
};
|
|
6
|
+
export declare function info(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: PostgresOptions): Promise<PostgresInfoResult>;
|
|
7
|
+
export declare const postgresExtensions: import("../../index.js").ResourceExtension<"data", "postgres", {
|
|
8
|
+
info: (appIdentity: string, addonIdentity?: string, options?: PostgresOptions) => Promise<PostgresInfoResult>;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { extendResource } from '../../core/extend-resource.js';
|
|
2
|
+
import { resolvePgDatabase } from './internal/resolve-pg-database.js';
|
|
3
|
+
export async function info(ctx, appIdentity, addonIdentity, options = {}) {
|
|
4
|
+
options.signal?.throwIfAborted();
|
|
5
|
+
const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
|
|
6
|
+
return ctx.data.postgres.info(addon.id);
|
|
7
|
+
}
|
|
8
|
+
export const postgresExtensions = extendResource('data', 'postgres', ctx => ({
|
|
9
|
+
info: (appIdentity, addonIdentity, options) => info(ctx, appIdentity, addonIdentity, options),
|
|
10
|
+
}));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TransferScheduleListResult } from '@heroku/types/data';
|
|
2
|
+
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
3
|
+
export type TransferScheduleOptions = {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
};
|
|
6
|
+
export declare function list(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: TransferScheduleOptions): Promise<TransferScheduleListResult>;
|
|
7
|
+
export declare const transferScheduleExtensions: import("../../index.js").ResourceExtension<"data", "transferSchedule", {
|
|
8
|
+
list: (appIdentity: string, addonIdentity?: string, options?: TransferScheduleOptions) => Promise<TransferScheduleListResult>;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { extendResource } from '../../core/extend-resource.js';
|
|
2
|
+
import { resolvePgDatabase } from './internal/resolve-pg-database.js';
|
|
3
|
+
export async function list(ctx, appIdentity, addonIdentity, options = {}) {
|
|
4
|
+
options.signal?.throwIfAborted();
|
|
5
|
+
const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
|
|
6
|
+
return ctx.data.transferSchedule.list(addon.id);
|
|
7
|
+
}
|
|
8
|
+
export const transferScheduleExtensions = extendResource('data', 'transferSchedule', ctx => ({
|
|
9
|
+
list: (appIdentity, addonIdentity, options) => list(ctx, appIdentity, addonIdentity, options),
|
|
10
|
+
}));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
2
|
+
export type TransferOptions = {
|
|
3
|
+
signal?: AbortSignal;
|
|
4
|
+
};
|
|
5
|
+
export type BackupTransfer = {
|
|
6
|
+
canceled_at: string;
|
|
7
|
+
created_at: string;
|
|
8
|
+
deleted_at: string;
|
|
9
|
+
finished_at: string;
|
|
10
|
+
from_name: string;
|
|
11
|
+
from_type: string;
|
|
12
|
+
from_url: string;
|
|
13
|
+
logs: Array<{
|
|
14
|
+
created_at: string;
|
|
15
|
+
level: string;
|
|
16
|
+
message: string;
|
|
17
|
+
}>;
|
|
18
|
+
num: number;
|
|
19
|
+
num_keep: number;
|
|
20
|
+
options: Record<string, unknown>;
|
|
21
|
+
processed_bytes: number;
|
|
22
|
+
purged_at: string;
|
|
23
|
+
schedule?: {
|
|
24
|
+
uuid: string;
|
|
25
|
+
};
|
|
26
|
+
source_bytes: number;
|
|
27
|
+
started_at: string;
|
|
28
|
+
succeeded: boolean;
|
|
29
|
+
to_name: string;
|
|
30
|
+
to_type: string;
|
|
31
|
+
to_url: string;
|
|
32
|
+
updated_at: string;
|
|
33
|
+
uuid: string;
|
|
34
|
+
warnings: number;
|
|
35
|
+
};
|
|
36
|
+
type TransferDataClient = {
|
|
37
|
+
transfer: {
|
|
38
|
+
listByApp(name: string): Promise<BackupTransfer[]>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
type TransferCtx = {
|
|
42
|
+
data: TransferDataClient;
|
|
43
|
+
platform: ResourceCtx['platform'];
|
|
44
|
+
};
|
|
45
|
+
export declare function listByApp(ctx: TransferCtx, appIdentity: string, addonIdentity?: string, options?: TransferOptions): Promise<BackupTransfer[]>;
|
|
46
|
+
export declare const transferExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "transfer", {
|
|
47
|
+
listByApp: (appIdentity: string, addonIdentity?: string, options?: TransferOptions) => Promise<BackupTransfer[]>;
|
|
48
|
+
}>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { extendResource } from '../../core/extend-resource.js';
|
|
2
|
+
import { resolvePgDatabase } from './internal/resolve-pg-database.js';
|
|
3
|
+
export async function listByApp(ctx, appIdentity, addonIdentity, options = {}) {
|
|
4
|
+
options.signal?.throwIfAborted();
|
|
5
|
+
const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
|
|
6
|
+
return ctx.data.transfer.listByApp(addon.id);
|
|
7
|
+
}
|
|
8
|
+
export const transferExtensions = extendResource('data', 'transfer', rawCtx => {
|
|
9
|
+
// Bridge heroku-types' loose client types to the adapter's strongly-typed contract
|
|
10
|
+
const ctx = rawCtx;
|
|
11
|
+
return {
|
|
12
|
+
listByApp: (appIdentity, addonIdentity, options) => listByApp(ctx, appIdentity, addonIdentity, options),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { addOnExtensions } from '../platform/add-on/index.js';
|
|
2
|
+
export { appExtensions, type AppInfo, type PipelineCouplingDetail } from '../platform/app/index.js';
|
|
3
|
+
export { dynoExtensions } from '../platform/dyno/index.js';
|
|
4
|
+
export { type AttachmentAttachedEvent, type AttachmentDetachedEvent, type AttachmentUpdatedEvent, type HerokuLogEvent, logSessionExtensions, parseHerokuLogLine, type ProvisioningCompletedEvent, type ScaledToEntry, type ScaledToEvent, type StartingProcessEvent, type StateChangedEvent, } from '../platform/log-session/index.js';
|
|
5
|
+
export { pipelineCouplingExtensions } from '../platform/pipeline/coupling.js';
|
|
6
|
+
export { pipelineExtensions } from '../platform/pipeline/index.js';
|
|
7
|
+
export { pipelinePromotionExtensions } from '../platform/pipeline/promotion.js';
|
|
8
|
+
export { privateToShield, shieldToPrivate } from '../platform/shield.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { addOnExtensions } from '../platform/add-on/index.js';
|
|
2
|
+
export { appExtensions } from '../platform/app/index.js';
|
|
3
|
+
export { dynoExtensions } from '../platform/dyno/index.js';
|
|
4
|
+
export { logSessionExtensions, parseHerokuLogLine, } from '../platform/log-session/index.js';
|
|
5
|
+
export { pipelineCouplingExtensions } from '../platform/pipeline/coupling.js';
|
|
6
|
+
export { pipelineExtensions } from '../platform/pipeline/index.js';
|
|
7
|
+
export { pipelinePromotionExtensions } from '../platform/pipeline/promotion.js';
|
|
8
|
+
export { privateToShield, shieldToPrivate } from '../platform/shield.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AddOn, AddOnCreateOpts } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
import type { CreateAndWaitOptions } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create an add-on and optionally wait for provisioning to complete.
|
|
6
|
+
*
|
|
7
|
+
* Wraps `addOn.create` with three pieces of orchestration:
|
|
8
|
+
*
|
|
9
|
+
* - 423 `confirmation_required` from the platform is converted to a
|
|
10
|
+
* typed `AddonConfirmationRequiredError`. Callers should catch
|
|
11
|
+
* this, prompt the user for confirmation, and retry the call with
|
|
12
|
+
* `body.confirm` set.
|
|
13
|
+
* - When `wait: true` is set, polls `addOn.infoByApp` on a
|
|
14
|
+
* `waitIntervalMs` cadence until the add-on's `state` is no longer
|
|
15
|
+
* `provisioning`. Throws `AddonProvisioningFailedError` if the
|
|
16
|
+
* terminal state is `deprovisioned`.
|
|
17
|
+
* - `options.onProvisioning` fires once after the create response
|
|
18
|
+
* when polling is about to begin, letting callers surface a
|
|
19
|
+
* two-phase status display ("Created" → "Waiting").
|
|
20
|
+
*
|
|
21
|
+
* Both the create and poll calls are issued with `Accept-Expansion:
|
|
22
|
+
* addon_service,plan` so the response includes the full plan shape
|
|
23
|
+
* (price, unit, etc.). The create call additionally sets
|
|
24
|
+
* `X-Heroku-Legacy-Provider-Messages: true` to opt into
|
|
25
|
+
* provider-supplied `provision_message` text on the response — without
|
|
26
|
+
* it the platform silently omits that field.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createAndWait(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, body: AddOnCreateOpts, options?: CreateAndWaitOptions): Promise<AddOn>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { HerokuApiError } from '@heroku/heroku-fetch';
|
|
2
|
+
import { wait } from '../../../utils/wait.js';
|
|
3
|
+
import { AddonConfirmationRequiredError, AddonProvisioningFailedError } from './errors.js';
|
|
4
|
+
const DEFAULT_CREATE_WAIT_INTERVAL_MS = 5000;
|
|
5
|
+
/**
|
|
6
|
+
* Create an add-on and optionally wait for provisioning to complete.
|
|
7
|
+
*
|
|
8
|
+
* Wraps `addOn.create` with three pieces of orchestration:
|
|
9
|
+
*
|
|
10
|
+
* - 423 `confirmation_required` from the platform is converted to a
|
|
11
|
+
* typed `AddonConfirmationRequiredError`. Callers should catch
|
|
12
|
+
* this, prompt the user for confirmation, and retry the call with
|
|
13
|
+
* `body.confirm` set.
|
|
14
|
+
* - When `wait: true` is set, polls `addOn.infoByApp` on a
|
|
15
|
+
* `waitIntervalMs` cadence until the add-on's `state` is no longer
|
|
16
|
+
* `provisioning`. Throws `AddonProvisioningFailedError` if the
|
|
17
|
+
* terminal state is `deprovisioned`.
|
|
18
|
+
* - `options.onProvisioning` fires once after the create response
|
|
19
|
+
* when polling is about to begin, letting callers surface a
|
|
20
|
+
* two-phase status display ("Created" → "Waiting").
|
|
21
|
+
*
|
|
22
|
+
* Both the create and poll calls are issued with `Accept-Expansion:
|
|
23
|
+
* addon_service,plan` so the response includes the full plan shape
|
|
24
|
+
* (price, unit, etc.). The create call additionally sets
|
|
25
|
+
* `X-Heroku-Legacy-Provider-Messages: true` to opt into
|
|
26
|
+
* provider-supplied `provision_message` text on the response — without
|
|
27
|
+
* it the platform silently omits that field.
|
|
28
|
+
*/
|
|
29
|
+
export async function createAndWait(ctx, appIdentity, body, options = {}) {
|
|
30
|
+
options.signal?.throwIfAborted();
|
|
31
|
+
const platform = ctx.platform.withHeaders({ 'Accept-Expansion': 'addon_service,plan' });
|
|
32
|
+
let addon;
|
|
33
|
+
try {
|
|
34
|
+
addon = await platform
|
|
35
|
+
.withHeaders({ 'X-Heroku-Legacy-Provider-Messages': 'true' })
|
|
36
|
+
.addOn.create(appIdentity, body);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof HerokuApiError && error.id === 'confirmation_required') {
|
|
40
|
+
throw new AddonConfirmationRequiredError(error.message);
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
if (!options.wait || addon.state !== 'provisioning') {
|
|
45
|
+
if (addon.state === 'deprovisioned') {
|
|
46
|
+
throw new AddonProvisioningFailedError(addon);
|
|
47
|
+
}
|
|
48
|
+
return addon;
|
|
49
|
+
}
|
|
50
|
+
await options.onProvisioning?.(addon);
|
|
51
|
+
const intervalMs = options.waitIntervalMs ?? DEFAULT_CREATE_WAIT_INTERVAL_MS;
|
|
52
|
+
while (addon.state === 'provisioning') {
|
|
53
|
+
options.signal?.throwIfAborted();
|
|
54
|
+
// eslint-disable-next-line no-await-in-loop
|
|
55
|
+
await wait(intervalMs, options.signal);
|
|
56
|
+
// eslint-disable-next-line no-await-in-loop
|
|
57
|
+
addon = await platform.addOn.infoByApp(appIdentity, addon.name);
|
|
58
|
+
}
|
|
59
|
+
if (addon.state === 'deprovisioned') {
|
|
60
|
+
throw new AddonProvisioningFailedError(addon);
|
|
61
|
+
}
|
|
62
|
+
return addon;
|
|
63
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
2
|
+
import type { DescribedAddOn, ResolveAddonOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve, describe, and decorate an add-on for display.
|
|
5
|
+
*
|
|
6
|
+
* - Resolves the add-on by identity (optionally scoped to an app).
|
|
7
|
+
* - Loads the add-on's attachments.
|
|
8
|
+
* - Returns a copy with `plan.price` set from `billed_price` (so the
|
|
9
|
+
* value reflects any grandfathered/contract pricing).
|
|
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
|
+
* Requests the `version=3.sdk` accept variant with `addon_service,plan`
|
|
17
|
+
* expansion so the resolved add-on includes the full `Plan` shape
|
|
18
|
+
* (`price.unit`, etc.) needed to render pricing.
|
|
19
|
+
*/
|
|
20
|
+
export declare function describeAddon(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, options?: ResolveAddonOptions): Promise<DescribedAddOn>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { resolveAddonInternal } from './resolve.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve, describe, and decorate an add-on for display.
|
|
4
|
+
*
|
|
5
|
+
* - Resolves the add-on by identity (optionally scoped to an app).
|
|
6
|
+
* - Loads the add-on's attachments.
|
|
7
|
+
* - Returns a copy with `plan.price` set from `billed_price` (so the
|
|
8
|
+
* value reflects any grandfathered/contract pricing).
|
|
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
|
+
* Requests the `version=3.sdk` accept variant with `addon_service,plan`
|
|
16
|
+
* expansion so the resolved add-on includes the full `Plan` shape
|
|
17
|
+
* (`price.unit`, etc.) needed to render pricing.
|
|
18
|
+
*/
|
|
19
|
+
export async function describeAddon(ctx, addonIdentity, options = {}) {
|
|
20
|
+
options.signal?.throwIfAborted();
|
|
21
|
+
// Accept-Expansion is scoped to the resolve call (addOn.resolution
|
|
22
|
+
// serializer accepts addon_service + plan expansions). The
|
|
23
|
+
// /addons/:id/addon-attachments endpoint uses Accept-Inclusion, not
|
|
24
|
+
// Accept-Expansion, and rejects an unknown expansion header.
|
|
25
|
+
const resolvingPlatform = ctx.platform.withHeaders({
|
|
26
|
+
Accept: 'application/vnd.heroku+json; version=3.sdk',
|
|
27
|
+
'Accept-Expansion': 'addon_service,plan',
|
|
28
|
+
});
|
|
29
|
+
const addon = await resolveAddonInternal(resolvingPlatform, addonIdentity, {
|
|
30
|
+
addonService: options.addonService,
|
|
31
|
+
appIdentity: options.appIdentity,
|
|
32
|
+
});
|
|
33
|
+
options.signal?.throwIfAborted();
|
|
34
|
+
const attachments = await ctx.platform.addOnAttachment.listByAddOn(addon.id);
|
|
35
|
+
const plan = addon.plan;
|
|
36
|
+
return {
|
|
37
|
+
...addon,
|
|
38
|
+
...(plan && { plan: { ...plan, price: grandfatheredPrice(addon) } }),
|
|
39
|
+
attachments,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function grandfatheredPrice(addon) {
|
|
43
|
+
const price = addon.plan?.price;
|
|
44
|
+
return {
|
|
45
|
+
...price,
|
|
46
|
+
...(addon.billed_price && {
|
|
47
|
+
cents: addon.billed_price.cents,
|
|
48
|
+
contract: addon.billed_price.contract,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AddOn } from '@heroku/types/3.sdk';
|
|
2
|
+
export declare class AddonNotFoundError extends Error {
|
|
3
|
+
readonly resource: string;
|
|
4
|
+
readonly id = "not_found";
|
|
5
|
+
readonly statusCode = 404;
|
|
6
|
+
constructor(resource?: string);
|
|
7
|
+
get body(): {
|
|
8
|
+
id: string;
|
|
9
|
+
message: string;
|
|
10
|
+
resource: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare class AddonAmbiguousError extends Error {
|
|
14
|
+
readonly matches: AddOn[];
|
|
15
|
+
readonly id = "multiple_matches";
|
|
16
|
+
readonly statusCode = 422;
|
|
17
|
+
constructor(matches: AddOn[]);
|
|
18
|
+
get body(): {
|
|
19
|
+
id: string;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by `createAndWait` when the Platform returns 423
|
|
25
|
+
* `confirmation_required`. Callers that want to prompt the user should
|
|
26
|
+
* catch this, gather a confirmation value, and retry the call with
|
|
27
|
+
* `body.confirm` set.
|
|
28
|
+
*/
|
|
29
|
+
export declare class AddonConfirmationRequiredError extends Error {
|
|
30
|
+
readonly platformMessage: string;
|
|
31
|
+
readonly id = "confirmation_required";
|
|
32
|
+
readonly statusCode = 423;
|
|
33
|
+
constructor(platformMessage: string);
|
|
34
|
+
get body(): {
|
|
35
|
+
id: string;
|
|
36
|
+
message: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Thrown by `createAndWait` when the Platform reports the add-on
|
|
41
|
+
* settled into a non-provisioned terminal state (e.g. `deprovisioned`).
|
|
42
|
+
*/
|
|
43
|
+
export declare class AddonProvisioningFailedError extends Error {
|
|
44
|
+
readonly addon: AddOn;
|
|
45
|
+
readonly id = "provisioning_failed";
|
|
46
|
+
constructor(addon: AddOn);
|
|
47
|
+
get body(): {
|
|
48
|
+
id: string;
|
|
49
|
+
message: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export class AddonNotFoundError extends Error {
|
|
2
|
+
resource;
|
|
3
|
+
id = 'not_found';
|
|
4
|
+
statusCode = 404;
|
|
5
|
+
constructor(resource = 'addon') {
|
|
6
|
+
super(`Couldn't find that ${resource}.`);
|
|
7
|
+
this.resource = resource;
|
|
8
|
+
this.name = 'AddonNotFoundError';
|
|
9
|
+
}
|
|
10
|
+
get body() {
|
|
11
|
+
return { id: this.id, message: this.message, resource: this.resource };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class AddonAmbiguousError extends Error {
|
|
15
|
+
matches;
|
|
16
|
+
id = 'multiple_matches';
|
|
17
|
+
statusCode = 422;
|
|
18
|
+
constructor(matches) {
|
|
19
|
+
super(`Ambiguous identifier; multiple matching add-ons found: ${matches.map(m => m.name).join(', ')}.`);
|
|
20
|
+
this.matches = matches;
|
|
21
|
+
this.name = 'AddonAmbiguousError';
|
|
22
|
+
}
|
|
23
|
+
get body() {
|
|
24
|
+
return { id: this.id, message: this.message };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Thrown by `createAndWait` when the Platform returns 423
|
|
29
|
+
* `confirmation_required`. Callers that want to prompt the user should
|
|
30
|
+
* catch this, gather a confirmation value, and retry the call with
|
|
31
|
+
* `body.confirm` set.
|
|
32
|
+
*/
|
|
33
|
+
export class AddonConfirmationRequiredError extends Error {
|
|
34
|
+
platformMessage;
|
|
35
|
+
id = 'confirmation_required';
|
|
36
|
+
statusCode = 423;
|
|
37
|
+
constructor(platformMessage) {
|
|
38
|
+
super(platformMessage);
|
|
39
|
+
this.platformMessage = platformMessage;
|
|
40
|
+
this.name = 'AddonConfirmationRequiredError';
|
|
41
|
+
}
|
|
42
|
+
get body() {
|
|
43
|
+
return { id: this.id, message: this.message };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Thrown by `createAndWait` when the Platform reports the add-on
|
|
48
|
+
* settled into a non-provisioned terminal state (e.g. `deprovisioned`).
|
|
49
|
+
*/
|
|
50
|
+
export class AddonProvisioningFailedError extends Error {
|
|
51
|
+
addon;
|
|
52
|
+
id = 'provisioning_failed';
|
|
53
|
+
constructor(addon) {
|
|
54
|
+
super(`The add-on was unable to be created, with status ${addon.state}.`);
|
|
55
|
+
this.addon = addon;
|
|
56
|
+
this.name = 'AddonProvisioningFailedError';
|
|
57
|
+
}
|
|
58
|
+
get body() {
|
|
59
|
+
return { id: this.id, message: this.message };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AddOnCreateOpts } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ListPlansForAddonOptions } from './list-plans.js';
|
|
3
|
+
import type { AddOnOptions, CreateAndWaitOptions, ResolveAddonOptions, UpgradeAddOnOptions } from './types.js';
|
|
4
|
+
import { formatPlanPriceLabel, priceForPlan } from './pricing.js';
|
|
5
|
+
export { createAndWait } from './create-and-wait.js';
|
|
6
|
+
export { describeAddon } from './describe.js';
|
|
7
|
+
export { AddonAmbiguousError, AddonConfirmationRequiredError, AddonNotFoundError, AddonProvisioningFailedError, } from './errors.js';
|
|
8
|
+
export { listPlans, listPlansForAddon, type ListPlansForAddonOptions } from './list-plans.js';
|
|
9
|
+
export { formatPlanPriceLabel, type FormatPlanPriceLabelOptions, type PlanPriceBreakdown, priceForPlan, } from './pricing.js';
|
|
10
|
+
export { resolveAddon, resolveAddonByAttachment } from './resolve.js';
|
|
11
|
+
export type { AddOnOptions, CreateAndWaitOptions, DescribedAddOn, ResolveAddonOptions, ResolvedAddOn, UpgradeAddOnOptions, } from './types.js';
|
|
12
|
+
export { upgrade } from './upgrade.js';
|
|
13
|
+
export declare const addOnExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "addOn", {
|
|
14
|
+
createAndWait: (appIdentity: string, body: AddOnCreateOpts, options?: CreateAndWaitOptions) => Promise<import("@heroku/types/3.sdk").AddOn>;
|
|
15
|
+
describe: (addonIdentity: string, options?: ResolveAddonOptions) => Promise<import("./types.js").DescribedAddOn>;
|
|
16
|
+
formatPlanPriceLabel: typeof formatPlanPriceLabel;
|
|
17
|
+
listPlans: (serviceIdentity: string, options?: AddOnOptions) => Promise<import("@heroku/types/3.sdk").Plan[]>;
|
|
18
|
+
listPlansForAddon: (addonIdentity: string, options?: ListPlansForAddonOptions) => Promise<import("@heroku/types/3.sdk").Plan[]>;
|
|
19
|
+
priceForPlan: typeof priceForPlan;
|
|
20
|
+
resolve: (addonIdentity: string, options?: ResolveAddonOptions) => Promise<import("./types.js").ResolvedAddOn>;
|
|
21
|
+
resolveByAttachment: (appIdentity: string, attachmentName: string, options?: AddOnOptions) => Promise<import("./types.js").ResolvedAddOn>;
|
|
22
|
+
upgrade: (addonIdentity: string, plan: string, options?: UpgradeAddOnOptions) => Promise<import("@heroku/types/3.sdk").AddOn>;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { createAndWait } from './create-and-wait.js';
|
|
3
|
+
import { describeAddon } from './describe.js';
|
|
4
|
+
import { listPlans, listPlansForAddon } from './list-plans.js';
|
|
5
|
+
import { formatPlanPriceLabel, priceForPlan } from './pricing.js';
|
|
6
|
+
import { resolveAddon, resolveAddonByAttachment } from './resolve.js';
|
|
7
|
+
import { upgrade } from './upgrade.js';
|
|
8
|
+
export { createAndWait } from './create-and-wait.js';
|
|
9
|
+
export { describeAddon } from './describe.js';
|
|
10
|
+
export { AddonAmbiguousError, AddonConfirmationRequiredError, AddonNotFoundError, AddonProvisioningFailedError, } from './errors.js';
|
|
11
|
+
export { listPlans, listPlansForAddon } from './list-plans.js';
|
|
12
|
+
export { formatPlanPriceLabel, priceForPlan, } from './pricing.js';
|
|
13
|
+
export { resolveAddon, resolveAddonByAttachment } from './resolve.js';
|
|
14
|
+
export { upgrade } from './upgrade.js';
|
|
15
|
+
export const addOnExtensions = extendResource('platform', 'addOn', ctx => ({
|
|
16
|
+
createAndWait: (appIdentity, body, options) => createAndWait(ctx, appIdentity, body, options),
|
|
17
|
+
describe: (addonIdentity, options) => describeAddon(ctx, addonIdentity, options),
|
|
18
|
+
formatPlanPriceLabel,
|
|
19
|
+
listPlans: (serviceIdentity, options) => listPlans(ctx, serviceIdentity, options),
|
|
20
|
+
listPlansForAddon: (addonIdentity, options) => listPlansForAddon(ctx, addonIdentity, options),
|
|
21
|
+
priceForPlan,
|
|
22
|
+
resolve: (addonIdentity, options) => resolveAddon(ctx, addonIdentity, options),
|
|
23
|
+
resolveByAttachment: (appIdentity, attachmentName, options) => resolveAddonByAttachment(ctx, appIdentity, attachmentName, options),
|
|
24
|
+
upgrade: (addonIdentity, plan, options) => upgrade(ctx, addonIdentity, plan, options),
|
|
25
|
+
}));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Plan } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
import type { AddOnOptions, ResolveAddonOptions } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Subset of {@link ResolveAddonOptions} accepted by
|
|
6
|
+
* {@link listPlansForAddon}. The `addonService` filter is intentionally
|
|
7
|
+
* omitted: once you've pinned an add-on identity, filtering its
|
|
8
|
+
* resolution by service name is nonsensical.
|
|
9
|
+
*/
|
|
10
|
+
export type ListPlansForAddonOptions = Pick<ResolveAddonOptions, 'appIdentity' | 'signal'>;
|
|
11
|
+
/**
|
|
12
|
+
* List the plans available for an add-on service, sorted ascending by
|
|
13
|
+
* `price.cents`. Plans without a price (or with equal prices) are
|
|
14
|
+
* returned later in the list.
|
|
15
|
+
*/
|
|
16
|
+
export declare function listPlans(ctx: Pick<ResourceCtx, 'platform'>, serviceIdentity: string, options?: AddOnOptions): Promise<Plan[]>;
|
|
17
|
+
/**
|
|
18
|
+
* List the plans available for the add-on service of an existing
|
|
19
|
+
* add-on, sorted ascending by `price.cents`.
|
|
20
|
+
*
|
|
21
|
+
* Convenience wrapper that resolves the add-on first to look up its
|
|
22
|
+
* `addon_service.name`, then delegates to `listPlans`. Saves callers
|
|
23
|
+
* the dance of fetching the add-on, plucking its service id/name, and
|
|
24
|
+
* passing it through.
|
|
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`)
|
|
30
|
+
*
|
|
31
|
+
* Pass `appIdentity` via `options` when the add-on identity is
|
|
32
|
+
* scoped to an app (same semantics as `resolveAddon`).
|
|
33
|
+
*/
|
|
34
|
+
export declare function listPlansForAddon(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, options?: ListPlansForAddonOptions): Promise<Plan[]>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { AddonNotFoundError } from './errors.js';
|
|
2
|
+
import { resolveAddonInternal } from './resolve.js';
|
|
3
|
+
/**
|
|
4
|
+
* List the plans available for an add-on service, sorted ascending by
|
|
5
|
+
* `price.cents`. Plans without a price (or with equal prices) are
|
|
6
|
+
* returned later in the list.
|
|
7
|
+
*/
|
|
8
|
+
export async function listPlans(ctx, serviceIdentity, options = {}) {
|
|
9
|
+
options.signal?.throwIfAborted();
|
|
10
|
+
const plans = await ctx.platform.plan.listByAddOn(serviceIdentity);
|
|
11
|
+
return [...plans].sort(byPriceCentsAsc);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* List the plans available for the add-on service of an existing
|
|
15
|
+
* add-on, sorted ascending by `price.cents`.
|
|
16
|
+
*
|
|
17
|
+
* Convenience wrapper that resolves the add-on first to look up its
|
|
18
|
+
* `addon_service.name`, then delegates to `listPlans`. Saves callers
|
|
19
|
+
* the dance of fetching the add-on, plucking its service id/name, and
|
|
20
|
+
* passing it through.
|
|
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`)
|
|
26
|
+
*
|
|
27
|
+
* Pass `appIdentity` via `options` when the add-on identity is
|
|
28
|
+
* scoped to an app (same semantics as `resolveAddon`).
|
|
29
|
+
*/
|
|
30
|
+
export async function listPlansForAddon(ctx, addonIdentity, options = {}) {
|
|
31
|
+
options.signal?.throwIfAborted();
|
|
32
|
+
const addon = await resolveAddonInternal(ctx.platform, addonIdentity, {
|
|
33
|
+
appIdentity: options.appIdentity,
|
|
34
|
+
});
|
|
35
|
+
// The route accepts either name or id; name is stable across
|
|
36
|
+
// environments and easier to identify in logs.
|
|
37
|
+
const serviceName = addon.addon_service?.name;
|
|
38
|
+
if (!serviceName) {
|
|
39
|
+
// The resolve found the add-on but the response was missing the
|
|
40
|
+
// service info we need to look up plans. Treat this as not-found
|
|
41
|
+
// for the caller's purpose (they wanted a plans list and we
|
|
42
|
+
// can't produce one).
|
|
43
|
+
throw new AddonNotFoundError();
|
|
44
|
+
}
|
|
45
|
+
return listPlans(ctx, serviceName, options);
|
|
46
|
+
}
|
|
47
|
+
function byPriceCentsAsc(a, b) {
|
|
48
|
+
return sortableCents(a) - sortableCents(b);
|
|
49
|
+
}
|
|
50
|
+
function sortableCents(plan) {
|
|
51
|
+
const cents = plan.price?.cents;
|
|
52
|
+
return typeof cents === 'number' ? cents : Number.MAX_SAFE_INTEGER;
|
|
53
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Plan } from '@heroku/types/3.sdk';
|
|
2
|
+
export type PlanPriceBreakdown = {
|
|
3
|
+
/** raw price as reported by the API, in cents */
|
|
4
|
+
cents: number;
|
|
5
|
+
/** True when the price is negotiated outside monthly add-on billing */
|
|
6
|
+
contract: boolean;
|
|
7
|
+
/** True when the price is billed per use rather than at a fixed cadence */
|
|
8
|
+
metered: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Equivalent hourly cost in cents (may be fractional). Equal to
|
|
11
|
+
* `cents` when `unit` is `'hour'`; computed as `cents /
|
|
12
|
+
* HOURS_PER_MONTH` when `unit` is `'month'`. `undefined` when the
|
|
13
|
+
* unit is anything else — callers can omit the per-hour label
|
|
14
|
+
* rather than render misleading numbers.
|
|
15
|
+
*/
|
|
16
|
+
perHourCents: number | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Equivalent monthly cost in cents. Equal to `cents` when `unit` is
|
|
19
|
+
* `'month'`; computed as `cents * HOURS_PER_MONTH` when `unit` is
|
|
20
|
+
* `'hour'`. `undefined` when the unit is anything else.
|
|
21
|
+
*/
|
|
22
|
+
perMonthCents: number | undefined;
|
|
23
|
+
/** unit reported by the API (e.g. `'month'`, `'hour'`) */
|
|
24
|
+
unit: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Break a plan's price down into monthly- and hourly-equivalent
|
|
28
|
+
* cents, normalizing across the API's billing unit. Consumers that
|
|
29
|
+
* display "max $X/month (~$Y/hour)" labels should call this rather
|
|
30
|
+
* than dividing `cents` by hours themselves; centralizing the math
|
|
31
|
+
* keeps the 30-day-month assumption (Heroku's documented convention)
|
|
32
|
+
* in one place.
|
|
33
|
+
*
|
|
34
|
+
* Returns `undefined` if the plan has no usable `price.cents` value.
|
|
35
|
+
* `Plan.price` is typed as optional in `@heroku/types`, and consumers
|
|
36
|
+
* occasionally receive partial responses depending on the Accept
|
|
37
|
+
* variant.
|
|
38
|
+
*
|
|
39
|
+
* `perMonthCents` and `perHourCents` are themselves `undefined` when
|
|
40
|
+
* the API reports a unit other than `'month'` or `'hour'` — callers
|
|
41
|
+
* should omit those parts of their label rather than show fabricated
|
|
42
|
+
* numbers.
|
|
43
|
+
*
|
|
44
|
+
* @param plan The plan to break down. Pass the whole `Plan` rather
|
|
45
|
+
* than just `plan.price` so the helper can pick up future fields
|
|
46
|
+
* without API churn at call sites.
|
|
47
|
+
*/
|
|
48
|
+
export declare function priceForPlan(plan: Plan): PlanPriceBreakdown | undefined;
|
|
49
|
+
export type FormatPlanPriceLabelOptions = {
|
|
50
|
+
/** ISO 4217 currency code passed to `Intl.NumberFormat`. Default: `'USD'`. */
|
|
51
|
+
currency?: string;
|
|
52
|
+
/** BCP 47 locale tag passed to `Intl.NumberFormat`. Default: `'en-US'`. */
|
|
53
|
+
locale?: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Format a plan's price as a human-readable suffix:
|
|
57
|
+
* `"$0.007 / hour (Max $5/month)"`. Returns an empty string when the
|
|
58
|
+
* plan has no displayable price (no `price.cents`, an unknown unit,
|
|
59
|
+
* a metered plan, or a contract-priced plan) — callers can compose
|
|
60
|
+
* their final label as `${name}${suffix ? ' - ' + suffix : ''}`
|
|
61
|
+
* without per-call defensive checks.
|
|
62
|
+
*
|
|
63
|
+
* Locale and currency default to `'en-US'` / `'USD'`. Pass options
|
|
64
|
+
* to override.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* const suffix = formatPlanPriceLabel(plan);
|
|
69
|
+
* const label = suffix ? `${plan.human_name} - ${suffix}` : plan.human_name;
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare function formatPlanPriceLabel(plan: Plan, options?: FormatPlanPriceLabelOptions): string;
|