@heroku/sdk 0.1.0-beta.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.
Files changed (109) hide show
  1. package/LICENSE.txt +207 -0
  2. package/README.md +94 -0
  3. package/dist/compositions/add-on.d.ts +7 -0
  4. package/dist/compositions/add-on.js +6 -0
  5. package/dist/compositions/app.d.ts +8 -0
  6. package/dist/compositions/app.js +11 -0
  7. package/dist/compositions/dyno.d.ts +17 -0
  8. package/dist/compositions/dyno.js +23 -0
  9. package/dist/compositions/pg.d.ts +15 -0
  10. package/dist/compositions/pg.js +56 -0
  11. package/dist/compositions/pipeline.d.ts +33 -0
  12. package/dist/compositions/pipeline.js +146 -0
  13. package/dist/core/create-client.d.ts +32 -0
  14. package/dist/core/create-client.js +48 -0
  15. package/dist/core/dispatcher.d.ts +3 -0
  16. package/dist/core/dispatcher.js +80 -0
  17. package/dist/core/extend-resource.d.ts +24 -0
  18. package/dist/core/extend-resource.js +3 -0
  19. package/dist/core/extensions-proxy.d.ts +2 -0
  20. package/dist/core/extensions-proxy.js +56 -0
  21. package/dist/core/heroku-sdk.d.ts +14 -0
  22. package/dist/core/heroku-sdk.js +54 -0
  23. package/dist/core/interpolate-path.d.ts +3 -0
  24. package/dist/core/interpolate-path.js +13 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +2 -0
  27. package/dist/resources/data/backup.d.ts +9 -0
  28. package/dist/resources/data/backup.js +10 -0
  29. package/dist/resources/data/database.d.ts +54 -0
  30. package/dist/resources/data/database.js +44 -0
  31. package/dist/resources/data/internal/resolve-addon-id.d.ts +2 -0
  32. package/dist/resources/data/internal/resolve-addon-id.js +13 -0
  33. package/dist/resources/data/internal/resolve-pg-database.d.ts +23 -0
  34. package/dist/resources/data/internal/resolve-pg-database.js +52 -0
  35. package/dist/resources/data/maintenance.d.ts +9 -0
  36. package/dist/resources/data/maintenance.js +10 -0
  37. package/dist/resources/data/postgres-credential.d.ts +11 -0
  38. package/dist/resources/data/postgres-credential.js +16 -0
  39. package/dist/resources/data/postgres-database.d.ts +32 -0
  40. package/dist/resources/data/postgres-database.js +14 -0
  41. package/dist/resources/data/postgres.d.ts +9 -0
  42. package/dist/resources/data/postgres.js +10 -0
  43. package/dist/resources/data/transfer-schedule.d.ts +9 -0
  44. package/dist/resources/data/transfer-schedule.js +10 -0
  45. package/dist/resources/data/transfer.d.ts +49 -0
  46. package/dist/resources/data/transfer.js +14 -0
  47. package/dist/resources/extensions/data.d.ts +4 -0
  48. package/dist/resources/extensions/data.js +4 -0
  49. package/dist/resources/extensions/platform.d.ts +8 -0
  50. package/dist/resources/extensions/platform.js +8 -0
  51. package/dist/resources/platform/add-on/create-and-wait.d.ts +28 -0
  52. package/dist/resources/platform/add-on/create-and-wait.js +63 -0
  53. package/dist/resources/platform/add-on/debug.d.ts +2 -0
  54. package/dist/resources/platform/add-on/debug.js +2 -0
  55. package/dist/resources/platform/add-on/describe.d.ts +20 -0
  56. package/dist/resources/platform/add-on/describe.js +51 -0
  57. package/dist/resources/platform/add-on/errors.d.ts +51 -0
  58. package/dist/resources/platform/add-on/errors.js +61 -0
  59. package/dist/resources/platform/add-on/index.d.ts +23 -0
  60. package/dist/resources/platform/add-on/index.js +25 -0
  61. package/dist/resources/platform/add-on/list-plans.d.ts +34 -0
  62. package/dist/resources/platform/add-on/list-plans.js +53 -0
  63. package/dist/resources/platform/add-on/pricing.d.ts +72 -0
  64. package/dist/resources/platform/add-on/pricing.js +100 -0
  65. package/dist/resources/platform/add-on/resolve.d.ts +43 -0
  66. package/dist/resources/platform/add-on/resolve.js +103 -0
  67. package/dist/resources/platform/add-on/types.d.ts +49 -0
  68. package/dist/resources/platform/add-on/types.js +1 -0
  69. package/dist/resources/platform/add-on/upgrade.d.ts +21 -0
  70. package/dist/resources/platform/add-on/upgrade.js +36 -0
  71. package/dist/resources/platform/add-on.d.ts +211 -0
  72. package/dist/resources/platform/add-on.js +354 -0
  73. package/dist/resources/platform/app/index.d.ts +58 -0
  74. package/dist/resources/platform/app/index.js +53 -0
  75. package/dist/resources/platform/app/info.d.ts +30 -0
  76. package/dist/resources/platform/app/info.js +27 -0
  77. package/dist/resources/platform/app.d.ts +56 -0
  78. package/dist/resources/platform/app.js +50 -0
  79. package/dist/resources/platform/dyno/index.d.ts +45 -0
  80. package/dist/resources/platform/dyno/index.js +26 -0
  81. package/dist/resources/platform/dyno.d.ts +45 -0
  82. package/dist/resources/platform/dyno.js +26 -0
  83. package/dist/resources/platform/log-session/index.d.ts +102 -0
  84. package/dist/resources/platform/log-session/index.js +223 -0
  85. package/dist/resources/platform/log-session/parser.d.ts +74 -0
  86. package/dist/resources/platform/log-session/parser.js +71 -0
  87. package/dist/resources/platform/log-session.d.ts +81 -0
  88. package/dist/resources/platform/log-session.js +167 -0
  89. package/dist/resources/platform/pipeline/coupling.d.ts +27 -0
  90. package/dist/resources/platform/pipeline/coupling.js +55 -0
  91. package/dist/resources/platform/pipeline/index.d.ts +53 -0
  92. package/dist/resources/platform/pipeline/index.js +75 -0
  93. package/dist/resources/platform/pipeline/promotion.d.ts +21 -0
  94. package/dist/resources/platform/pipeline/promotion.js +109 -0
  95. package/dist/resources/platform/pipeline-coupling.d.ts +27 -0
  96. package/dist/resources/platform/pipeline-coupling.js +55 -0
  97. package/dist/resources/platform/pipeline-promotion.d.ts +21 -0
  98. package/dist/resources/platform/pipeline-promotion.js +128 -0
  99. package/dist/resources/platform/pipeline.d.ts +53 -0
  100. package/dist/resources/platform/pipeline.js +75 -0
  101. package/dist/resources/platform/shield.d.ts +2 -0
  102. package/dist/resources/platform/shield.js +6 -0
  103. package/dist/services/data.d.ts +5 -0
  104. package/dist/services/data.js +5 -0
  105. package/dist/services/platform.d.ts +5 -0
  106. package/dist/services/platform.js +5 -0
  107. package/dist/utils/wait.d.ts +16 -0
  108. package/dist/utils/wait.js +35 -0
  109. package/package.json +54 -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
+ }>;