@heroku/sdk 0.12.3 → 0.12.4

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.
@@ -1,5 +1,7 @@
1
- import type { PostgresRunUpgradeOpts, PostgresRunUpgradeResult } from '@heroku/types/data';
1
+ import type { AddOn } from '@heroku/types/3.sdk';
2
+ import type { PostgresPoolCreateResult, PostgresRunUpgradeOpts, PostgresRunUpgradeResult, PostgresWaitStatusResult } from '@heroku/types/data';
2
3
  import type { ResourceCtx } from '../../core/extend-resource.js';
4
+ import type { ResolvedAddOn } from '../platform/add-on/types.js';
3
5
  import type { ResolvedPgDatabase } from './internal/resolve-pg-database.js';
4
6
  export type RunUpgradeOptions = {
5
7
  beforeRunUpgrade?: (preview: {
@@ -22,6 +24,206 @@ export type RunUpgradeResult = {
22
24
  * upgrade.
23
25
  */
24
26
  export declare function runUpgrade(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, body?: PostgresRunUpgradeOpts, options?: RunUpgradeOptions): Promise<RunUpgradeResult>;
27
+ export type WaitForReadyOptions = {
28
+ /**
29
+ * How many 404s from `wait_status` to tolerate as "still provisioning"
30
+ * before rethrowing. Defaults to 20; this is a total budget across the whole
31
+ * wait, not reset between polls.
32
+ */
33
+ notFoundRetries?: number;
34
+ /**
35
+ * Fires once per poll with the latest status (including the synthetic
36
+ * "still provisioning" status produced while the endpoint 404s), letting
37
+ * callers surface a live progress message.
38
+ */
39
+ onPoll?: (status: PostgresWaitStatusResult) => void;
40
+ /**
41
+ * Fires once the database is resolved and tier-checked, before the wait
42
+ * poll begins, letting callers label a spinner with the resolved add-on
43
+ * (mirrors `provisionAdvanced`/`fork`'s `onProvisioning`). An abort during
44
+ * the hook prevents the poll.
45
+ */
46
+ onResolved?: (addon: ResolvedPgDatabase) => Promise<void> | void;
47
+ /**
48
+ * Aborts the in-flight request and any pending poll delay.
49
+ */
50
+ signal?: AbortSignal;
51
+ /**
52
+ * Maximum total time to wait before throwing. Defaults to 1 hour as a
53
+ * backstop against a `wait_status` that never reports ready; pass a larger
54
+ * value for a legitimately longer wait, or `Infinity` to poll until the
55
+ * database reports ready or `signal` aborts.
56
+ */
57
+ timeoutMs?: number;
58
+ /**
59
+ * Polling interval in milliseconds. Defaults to 5000 (5s), matching the
60
+ * CLI's historical `--wait-interval` default. Named `waitIntervalMs` to match
61
+ * the SDK-wide wait-tuning convention (`platform.addOn`, `platform.domain`,
62
+ * `data.postgresDatabase`).
63
+ */
64
+ waitIntervalMs?: number;
65
+ };
66
+ export type WaitForReadyResult = {
67
+ addon: ResolvedPgDatabase;
68
+ result: PostgresWaitStatusResult;
69
+ };
70
+ export type ProvisionAdvancedBody = {
71
+ /** Attachment name (`--as`). */
72
+ as?: string;
73
+ /**
74
+ * Extra provision options (parsed CLI `--provision-option` pairs). Merged
75
+ * last, so these override `level`/`version`/`high-availability`.
76
+ */
77
+ config?: Record<string, boolean | string | undefined>;
78
+ /** Confirmation value for a `confirmation_required` retry. */
79
+ confirm?: string;
80
+ /** Number of follower pool instances to provision after the leader. */
81
+ followers?: number;
82
+ /** Provision the leader with a high-availability standby. */
83
+ highAvailability?: boolean;
84
+ /** Leader compute level (e.g. `advanced-0`). */
85
+ level: string;
86
+ /** Add-on name (`--name`). */
87
+ name?: string;
88
+ /** Private/Shield network variant; selects the `advanced[-private|-shield]` plan. */
89
+ network?: 'private' | 'shield';
90
+ /** Postgres version to provision. */
91
+ version?: string;
92
+ };
93
+ export type ForkBody = {
94
+ /** Attachment name (`--as`). */
95
+ as?: string;
96
+ /** Extra provision options (parsed CLI `--provision-option` pairs). Merged last. */
97
+ config?: Record<string, boolean | string | undefined>;
98
+ /** Confirmation value for a `confirmation_required` retry. */
99
+ confirm?: string;
100
+ /**
101
+ * Leader compute level. When omitted, defaults to the source database's
102
+ * current leader pool level.
103
+ */
104
+ level?: string;
105
+ /** Add-on name (`--name`). */
106
+ name?: string;
107
+ /**
108
+ * When set, provisions a point-in-time rollback instead of a live fork. A
109
+ * `Date` is formatted to the platform's UTC `YYYY-MM-DDTHH:MM:SS`; a string
110
+ * is passed through verbatim (matching the CLI's `--rollback-to`).
111
+ */
112
+ recoveryTime?: Date | string;
113
+ };
114
+ export type ProvisionAdvancedOptions = Pick<WaitForReadyOptions, 'notFoundRetries' | 'onPoll' | 'signal' | 'timeoutMs' | 'waitIntervalMs'> & {
115
+ /**
116
+ * Fires once the leader add-on create request returns, before the wait
117
+ * poll begins (only when `wait` is set), letting callers switch to a
118
+ * "waiting for provisioning" display.
119
+ */
120
+ onProvisioning?: (addon: AddOn) => Promise<void> | void;
121
+ /** Poll `wait_status` until the leader is ready before returning. */
122
+ wait?: boolean;
123
+ };
124
+ export type ForkOptions = ProvisionAdvancedOptions & {
125
+ /**
126
+ * Fires once the source database is resolved and tier-checked, before the
127
+ * fork create request is sent, letting callers defer a spinner until the
128
+ * tier check passes and label it with the resolved source add-on rather than
129
+ * the raw identity (mirrors `waitForReady`'s `onResolved`). An abort during
130
+ * the hook prevents the create.
131
+ */
132
+ onResolved?: (source: ResolvedAddOn) => Promise<void> | void;
133
+ };
134
+ export type ProvisionAdvancedResult = {
135
+ /**
136
+ * The newly-created leader add-on. Always present: the leader is billed the
137
+ * moment creation succeeds, so it is reported even when a later step
138
+ * (`wait`/follower pool) fails, letting callers surface what was provisioned
139
+ * instead of retrying and provisioning a second leader.
140
+ */
141
+ addon: AddOn;
142
+ /** The follower pool, when `followers` was provided and its creation succeeded. */
143
+ pool?: PostgresPoolCreateResult;
144
+ /**
145
+ * The error thrown while creating the follower pool, when `followers` was
146
+ * provided and creation failed. The leader (`addon`) still provisioned.
147
+ */
148
+ poolError?: unknown;
149
+ /**
150
+ * The error thrown while waiting for the leader to become ready, when `wait`
151
+ * was set and the wait failed (timeout, abort, 404-budget exhaustion, or a
152
+ * transient API error). The leader (`addon`) still provisioned; the follower
153
+ * pool is not attempted in this case.
154
+ */
155
+ waitError?: unknown;
156
+ /** The terminal wait status, when `wait` was set and the wait succeeded. */
157
+ waitStatus?: PostgresWaitStatusResult;
158
+ };
159
+ export type ForkResult = {
160
+ /**
161
+ * The newly-created fork/rollback add-on. Always present once creation
162
+ * succeeds, even if the subsequent `wait` fails (see
163
+ * {@link ProvisionAdvancedResult.addon}).
164
+ */
165
+ addon: AddOn;
166
+ /** The resolved source database the fork was created from. */
167
+ source: ResolvedAddOn;
168
+ /**
169
+ * The error thrown while waiting for the fork to become ready, when `wait`
170
+ * was set and the wait failed. The fork (`addon`) still provisioned.
171
+ */
172
+ waitError?: unknown;
173
+ /** The terminal wait status, when `wait` was set and the wait succeeded. */
174
+ waitStatus?: PostgresWaitStatusResult;
175
+ };
176
+ /**
177
+ * Resolves a Postgres database by attachment identity and polls its
178
+ * `wait_status` until it reports ready. `waiting: true` covers any in-progress
179
+ * operation (provisioning, migration, or update), not just initial provision.
180
+ *
181
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved add-on) if the
182
+ * database isn't an Advanced-tier database.
183
+ */
184
+ export declare function waitForReady(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: WaitForReadyOptions): Promise<WaitForReadyResult>;
185
+ /**
186
+ * Provisions a new Advanced-tier Postgres leader (and, when `followers` is
187
+ * given, a follower pool), replacing the CLI's `createAddon` + `createPool`
188
+ * sequence.
189
+ *
190
+ * A `confirmation_required` response surfaces as `AddonConfirmationRequiredError`
191
+ * — catch it, gather a confirmation value, and retry with `body.confirm` set
192
+ * (the CLI retries with `confirm = app`). When `wait` is set, polls the data
193
+ * `wait_status` endpoint (not the platform add-on state) until the leader is
194
+ * ready; the follower pool is then created against the ready leader, matching
195
+ * the CLI's ordering.
196
+ *
197
+ * **Partial failure.** Once the leader is created it is billed, so failures
198
+ * after creation are not thrown — they are reported on the result so the caller
199
+ * never loses track of what it provisioned. A failed `wait` sets `waitError`
200
+ * (and the follower pool is not attempted); a failed follower-pool create sets
201
+ * `poolError`. `addon` is always present on a returned result. A failure of the
202
+ * leader create itself still throws (nothing was provisioned). The wait polls
203
+ * the data `wait_status` endpoint rather than the platform add-on state, so it
204
+ * is bounded by a default `timeoutMs` (see {@link WaitForReadyOptions}) as a
205
+ * backstop against a platform-side provisioning failure that never surfaces on
206
+ * `wait_status`; pass a larger `timeoutMs` for a longer wait.
207
+ */
208
+ export declare function provisionAdvanced(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, body: ProvisionAdvancedBody, options?: ProvisionAdvancedOptions): Promise<ProvisionAdvancedResult>;
209
+ /**
210
+ * Forks (or, when `recoveryTime` is set, rolls back) an existing Advanced-tier
211
+ * Postgres database into a new add-on on `appIdentity`.
212
+ *
213
+ * `sourceIdentity` is both resolved (to tier-check, default the level, and
214
+ * pick the plan) and passed verbatim into the `fork`/`rollback` config value,
215
+ * matching the CLI, which forks against the raw user-supplied identity. The
216
+ * new add-on inherits the source's plan.
217
+ *
218
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved source) if the
219
+ * source isn't an Advanced-tier database. `onResolved` fires after a
220
+ * successful tier check and before the create request. Confirmation and wait
221
+ * semantics match `provisionAdvanced`.
222
+ */
223
+ export declare function fork(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, sourceIdentity: string, body?: ForkBody, options?: ForkOptions): Promise<ForkResult>;
25
224
  export declare const postgresExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "postgres", {
225
+ fork: (appIdentity: string, sourceIdentity: string, body?: ForkBody, options?: ForkOptions) => Promise<ForkResult>;
226
+ provisionAdvanced: (appIdentity: string, body: ProvisionAdvancedBody, options?: ProvisionAdvancedOptions) => Promise<ProvisionAdvancedResult>;
26
227
  runUpgrade: (appIdentity: string, addonIdentity?: string, body?: PostgresRunUpgradeOpts, options?: RunUpgradeOptions) => Promise<RunUpgradeResult>;
228
+ waitForReady: (appIdentity: string, addonIdentity?: string, options?: WaitForReadyOptions) => Promise<WaitForReadyResult>;
27
229
  }>;
@@ -1,7 +1,34 @@
1
+ import { NotFoundError } from '@heroku/heroku-fetch';
1
2
  import { extendResource } from '../../core/extend-resource.js';
2
3
  import { withSignal } from '../../core/with-signal.js';
3
- import { isAdvancedDatabase, NotAdvancedDatabaseError } from './internal/postgres-utils.js';
4
+ import { wait } from '../../utils/wait.js';
5
+ import { createAndWait } from '../platform/add-on/create-and-wait.js';
6
+ import { resolveAddonInternal } from '../platform/add-on/resolve.js';
7
+ import { getPostgresServiceName, isAdvancedDatabase, NotAdvancedDatabaseError } from './internal/postgres-utils.js';
4
8
  import { resolvePgDatabase } from './internal/resolve-pg-database.js';
9
+ import { LEADER_POOL_NAME } from './postgres-pool.js';
10
+ const DEFAULT_WAIT_INTERVAL_MS = 5000;
11
+ /**
12
+ * Default upper bound on a single wait before it throws. A backstop against a
13
+ * `wait_status` that never flips to ready — e.g. a platform-side provisioning
14
+ * failure the data endpoint doesn't reflect, or an endpoint that keeps
15
+ * reporting `waiting: true` — so polling can't run unbounded. Generous enough
16
+ * (1 hour) to cover large provisions and forks; callers who legitimately need
17
+ * longer can pass a larger `timeoutMs`, and `Infinity` disables the bound.
18
+ */
19
+ const DEFAULT_WAIT_TIMEOUT_MS = 60 * 60 * 1000;
20
+ /**
21
+ * Matches the CLI's historical 404 tolerance in `data:pg:wait`: the platform
22
+ * briefly 404s a freshly-provisioned database's `wait_status` before its row
23
+ * exists. We treat up to this many 404s as "still provisioning" (the budget is
24
+ * never reset), then rethrow.
25
+ */
26
+ const DEFAULT_WAIT_NOT_FOUND_RETRIES = 20;
27
+ /**
28
+ * Synthetic wait message surfaced to `onPoll` while the `wait_status` endpoint
29
+ * is still 404ing during the initial provisioning window.
30
+ */
31
+ const PROVISIONING_WAIT_MESSAGE = 'Waiting to provision...';
5
32
  /**
6
33
  * Resolves a Postgres database by attachment identity and runs a version upgrade.
7
34
  *
@@ -26,6 +53,221 @@ export async function runUpgrade(ctx, appIdentity, addonIdentity, body = {}, opt
26
53
  const result = await data.postgres.runUpgrade(addon.id, body);
27
54
  return { addon, result };
28
55
  }
56
+ /**
57
+ * Polls `data.postgres.waitStatus` for an add-on id until it reports ready
58
+ * (`waiting: false`), tolerating a bounded number of 404s during the initial
59
+ * provisioning window (see `notFoundRetries`). Shared by `waitForReady`,
60
+ * `provisionAdvanced`, and `fork`.
61
+ *
62
+ * The caller's `signal` is threaded into both the in-flight request (via
63
+ * `withSignal`) and the poll-delay `wait()`, so aborting takes effect during
64
+ * either phase.
65
+ */
66
+ async function pollPostgresReady(ctx, addonId, options = {}) {
67
+ const { notFoundRetries = DEFAULT_WAIT_NOT_FOUND_RETRIES, onPoll, signal, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS, waitIntervalMs = DEFAULT_WAIT_INTERVAL_MS, } = options;
68
+ signal?.throwIfAborted();
69
+ const data = withSignal(ctx.data, signal);
70
+ // `Infinity` (deadline === Infinity) disables the bound — the check below is
71
+ // never true — letting callers opt out of the default timeout explicitly.
72
+ const deadline = Date.now() + timeoutMs;
73
+ let remainingNotFound = notFoundRetries;
74
+ /* eslint-disable no-await-in-loop */
75
+ while (true) {
76
+ signal?.throwIfAborted();
77
+ let status;
78
+ try {
79
+ status = await data.postgres.waitStatus(addonId);
80
+ }
81
+ catch (error) {
82
+ // The platform briefly 404s a freshly-provisioned database before its
83
+ // row exists; absorb a bounded number of those as "still provisioning".
84
+ if (!(error instanceof NotFoundError) || remainingNotFound <= 0)
85
+ throw error;
86
+ remainingNotFound--;
87
+ status = { message: PROVISIONING_WAIT_MESSAGE, waiting: true };
88
+ }
89
+ onPoll?.(status);
90
+ if (!status.waiting)
91
+ return status;
92
+ if (Date.now() >= deadline) {
93
+ throw new Error(`Postgres database '${addonId}' did not become ready within ${timeoutMs}ms`);
94
+ }
95
+ await wait(waitIntervalMs, signal);
96
+ }
97
+ /* eslint-enable no-await-in-loop */
98
+ }
99
+ /**
100
+ * Resolves a Postgres database by attachment identity and polls its
101
+ * `wait_status` until it reports ready. `waiting: true` covers any in-progress
102
+ * operation (provisioning, migration, or update), not just initial provision.
103
+ *
104
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved add-on) if the
105
+ * database isn't an Advanced-tier database.
106
+ */
107
+ export async function waitForReady(ctx, appIdentity, addonIdentity, options = {}) {
108
+ const { onResolved, signal } = options;
109
+ signal?.throwIfAborted();
110
+ const addon = await resolvePgDatabase(ctx, appIdentity, { input: addonIdentity, signal });
111
+ if (!isAdvancedDatabase(addon)) {
112
+ throw new NotAdvancedDatabaseError(addon, `You can only use postgres.waitForReady() on Advanced-tier databases. ${addon.name} isn't an Advanced-tier database.`);
113
+ }
114
+ await onResolved?.(addon);
115
+ signal?.throwIfAborted();
116
+ const result = await pollPostgresReady(ctx, addon.id, options);
117
+ return { addon, result };
118
+ }
119
+ /**
120
+ * Builds a provision config body, dropping `undefined` values (the platform
121
+ * treats an absent key as unset) and letting `extra` options override the
122
+ * well-known keys. Booleans are preserved and serialized as-is.
123
+ */
124
+ function buildProvisionConfig(base, extra = {}) {
125
+ const config = {};
126
+ for (const [key, value] of Object.entries({ ...base, ...extra })) {
127
+ if (value !== undefined)
128
+ config[key] = value;
129
+ }
130
+ // The platform accepts booleans here (e.g. `high-availability`), but the
131
+ // typed `AddOnCreateOpts.config` is `Record<string, string>`; cast through,
132
+ // matching the CLI's own `createAddon` helper.
133
+ return config;
134
+ }
135
+ /**
136
+ * The shared "wait for the freshly-created leader/fork to become ready" step
137
+ * used by {@link provisionAdvanced} and {@link fork}. Returns `{}` when `wait`
138
+ * isn't set.
139
+ *
140
+ * The add-on has already been created (and billed) by the time this runs, so a
141
+ * wait failure — timeout, abort, 404-budget exhaustion, or a transient API
142
+ * error — is captured as `waitError` and returned rather than thrown. That lets
143
+ * the caller still report the created add-on instead of losing track of it.
144
+ */
145
+ async function waitAfterCreate(ctx, addon, options) {
146
+ const { notFoundRetries, onPoll, onProvisioning, signal, timeoutMs, wait: shouldWait, waitIntervalMs } = options;
147
+ if (!shouldWait)
148
+ return {};
149
+ try {
150
+ await onProvisioning?.(addon);
151
+ signal?.throwIfAborted();
152
+ const waitStatus = await pollPostgresReady(ctx, addon.id, {
153
+ notFoundRetries, onPoll, signal, timeoutMs, waitIntervalMs,
154
+ });
155
+ return { waitStatus };
156
+ }
157
+ catch (error) {
158
+ return { waitError: error };
159
+ }
160
+ }
161
+ /**
162
+ * Provisions a new Advanced-tier Postgres leader (and, when `followers` is
163
+ * given, a follower pool), replacing the CLI's `createAddon` + `createPool`
164
+ * sequence.
165
+ *
166
+ * A `confirmation_required` response surfaces as `AddonConfirmationRequiredError`
167
+ * — catch it, gather a confirmation value, and retry with `body.confirm` set
168
+ * (the CLI retries with `confirm = app`). When `wait` is set, polls the data
169
+ * `wait_status` endpoint (not the platform add-on state) until the leader is
170
+ * ready; the follower pool is then created against the ready leader, matching
171
+ * the CLI's ordering.
172
+ *
173
+ * **Partial failure.** Once the leader is created it is billed, so failures
174
+ * after creation are not thrown — they are reported on the result so the caller
175
+ * never loses track of what it provisioned. A failed `wait` sets `waitError`
176
+ * (and the follower pool is not attempted); a failed follower-pool create sets
177
+ * `poolError`. `addon` is always present on a returned result. A failure of the
178
+ * leader create itself still throws (nothing was provisioned). The wait polls
179
+ * the data `wait_status` endpoint rather than the platform add-on state, so it
180
+ * is bounded by a default `timeoutMs` (see {@link WaitForReadyOptions}) as a
181
+ * backstop against a platform-side provisioning failure that never surfaces on
182
+ * `wait_status`; pass a larger `timeoutMs` for a longer wait.
183
+ */
184
+ export async function provisionAdvanced(ctx, appIdentity, body, options = {}) {
185
+ const { signal } = options;
186
+ signal?.throwIfAborted();
187
+ const { as, config: extra, confirm, followers, highAvailability, level, name, network, version } = body;
188
+ const plan = `${getPostgresServiceName()}:advanced${network ? `-${network}` : ''}`;
189
+ const config = buildProvisionConfig({ 'high-availability': highAvailability, level, version }, extra);
190
+ const addon = await createAndWait(ctx, appIdentity, {
191
+ attachment: { name: as }, config, confirm, name, plan,
192
+ }, { signal });
193
+ const result = { addon };
194
+ const { waitError, waitStatus } = await waitAfterCreate(ctx, addon, options);
195
+ if (waitError !== undefined) {
196
+ result.waitError = waitError;
197
+ return result;
198
+ }
199
+ if (waitStatus !== undefined)
200
+ result.waitStatus = waitStatus;
201
+ if (followers && followers > 0) {
202
+ try {
203
+ result.pool = await withSignal(ctx.data, signal).postgresPool.create(addon.id, { count: followers, level });
204
+ }
205
+ catch (error) {
206
+ result.poolError = error;
207
+ }
208
+ }
209
+ return result;
210
+ }
211
+ const pad2 = (n) => String(n).padStart(2, '0');
212
+ /**
213
+ * Formats a rollback recovery time to the platform's UTC `YYYY-MM-DDTHH:MM:SS`
214
+ * (no `Z`, no milliseconds). A string is passed through unchanged.
215
+ */
216
+ function formatRecoveryTime(value) {
217
+ if (typeof value === 'string')
218
+ return value;
219
+ return `${value.getUTCFullYear()}-${pad2(value.getUTCMonth() + 1)}-${pad2(value.getUTCDate())}T${pad2(value.getUTCHours())}:${pad2(value.getUTCMinutes())}:${pad2(value.getUTCSeconds())}`;
220
+ }
221
+ /**
222
+ * Forks (or, when `recoveryTime` is set, rolls back) an existing Advanced-tier
223
+ * Postgres database into a new add-on on `appIdentity`.
224
+ *
225
+ * `sourceIdentity` is both resolved (to tier-check, default the level, and
226
+ * pick the plan) and passed verbatim into the `fork`/`rollback` config value,
227
+ * matching the CLI, which forks against the raw user-supplied identity. The
228
+ * new add-on inherits the source's plan.
229
+ *
230
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved source) if the
231
+ * source isn't an Advanced-tier database. `onResolved` fires after a
232
+ * successful tier check and before the create request. Confirmation and wait
233
+ * semantics match `provisionAdvanced`.
234
+ */
235
+ export async function fork(ctx, appIdentity, sourceIdentity, body = {}, options = {}) {
236
+ const { onResolved, signal } = options;
237
+ signal?.throwIfAborted();
238
+ const { as, config: extra, confirm, name, recoveryTime } = body;
239
+ const platform = withSignal(ctx.platform, signal);
240
+ const source = await resolveAddonInternal(platform, sourceIdentity, { addonService: getPostgresServiceName(), appIdentity });
241
+ if (!isAdvancedDatabase(source)) {
242
+ throw new NotAdvancedDatabaseError(source, `You can only fork Advanced-tier databases with postgres.fork(). ${source.name} isn't an Advanced-tier database.`);
243
+ }
244
+ await onResolved?.(source);
245
+ let { level } = body;
246
+ if (level === undefined) {
247
+ const info = await withSignal(ctx.data, signal).postgres.info(source.id);
248
+ const leaderPool = info.pools.find(pool => pool.name === LEADER_POOL_NAME);
249
+ if (leaderPool === undefined) {
250
+ throw new Error(`Cannot default the fork level: source database '${source.name}' has no '${LEADER_POOL_NAME}' pool. Pass an explicit 'level'.`);
251
+ }
252
+ level = leaderPool.expected_level;
253
+ }
254
+ const config = recoveryTime === undefined
255
+ ? buildProvisionConfig({ fork: sourceIdentity, level }, extra)
256
+ : buildProvisionConfig({ level, 'recovery-time': formatRecoveryTime(recoveryTime), rollback: sourceIdentity }, extra);
257
+ const addon = await createAndWait(ctx, appIdentity, {
258
+ attachment: { name: as }, config, confirm, name, plan: source.plan.name,
259
+ }, { signal });
260
+ const result = { addon, source };
261
+ const { waitError, waitStatus } = await waitAfterCreate(ctx, addon, options);
262
+ if (waitError !== undefined)
263
+ result.waitError = waitError;
264
+ else if (waitStatus !== undefined)
265
+ result.waitStatus = waitStatus;
266
+ return result;
267
+ }
29
268
  export const postgresExtensions = extendResource('data', 'postgres', ctx => ({
269
+ fork: (appIdentity, sourceIdentity, body, options) => fork(ctx, appIdentity, sourceIdentity, body, options),
270
+ provisionAdvanced: (appIdentity, body, options) => provisionAdvanced(ctx, appIdentity, body, options),
30
271
  runUpgrade: (appIdentity, addonIdentity, body, options) => runUpgrade(ctx, appIdentity, addonIdentity, body, options),
272
+ waitForReady: (appIdentity, addonIdentity, options) => waitForReady(ctx, appIdentity, addonIdentity, options),
31
273
  }));
@@ -4,6 +4,6 @@ export { RedisAddonAmbiguousError, RedisAddonNotFoundError } from './errors.js';
4
4
  export { resolveRedisByApp, type ResolveRedisByAppOptions } from './resolve-by-app.js';
5
5
  export { waitForRedisReady, type WaitForRedisReadyOptions } from './wait-for-ready.js';
6
6
  export declare const redisExtensions: import("../../../core/extend-resource.js").ResourceExtension<"data", "redis", {
7
- resolveByApp: (appIdentity: string, options?: ResolveRedisByAppOptions) => Promise<import("../../platform/add-on/types.js").ResolvedAddOn>;
7
+ resolveByApp: (appIdentity: string, options?: ResolveRedisByAppOptions) => Promise<import("../postgres-database.js").ResolvedAddOn>;
8
8
  waitForReady: (nameOrId: string, options?: WaitForRedisReadyOptions) => Promise<import("@heroku/types/data").RedisWaitResult>;
9
9
  }>;
@@ -2,8 +2,11 @@ export { databaseCredentialExtensions } from '../data/database-credential/index.
2
2
  export { databaseExtensions } from '../data/database.js';
3
3
  export { maintenanceExtensions } from '../data/maintenance.js';
4
4
  export { postgresDatabaseExtensions } from '../data/postgres-database.js';
5
+ export { isCancellable, isRunnable, type Migration, type MigrationContext, type MigrationCreateBody, MigrationInvalidError, type MigrationInvalidReason, MigrationNotCancellableError, MigrationNotRunnableError, type MigrationOptions, type MigrationSource, type MigrationStatus, type MigrationTarget, postgresMigrationExtensions, } from '../data/postgres-migration.js';
6
+ export { type AvailableLevel, LEADER_POOL_NAME, type LevelItem, type ListAvailableActionsOptions, MAX_POOL_INSTANCES, type PoolAction, postgresPoolExtensions, } from '../data/postgres-pool.js';
5
7
  export { postgresQuotaExtensions } from '../data/postgres-quota.js';
6
- export { postgresExtensions } from '../data/postgres.js';
8
+ export { postgresSettingsExtensions } from '../data/postgres-settings.js';
9
+ export { type ForkBody, type ForkResult, postgresExtensions, type WaitForReadyOptions as PostgresWaitForReadyOptions, type WaitForReadyResult as PostgresWaitForReadyResult, type ProvisionAdvancedBody, type ProvisionAdvancedOptions, type ProvisionAdvancedResult, type RunUpgradeOptions, type RunUpgradeResult, } from '../data/postgres.js';
7
10
  export { redisExtensions } from '../data/redis/index.js';
8
11
  export { type TransferSchedule, transferScheduleExtensions } from '../data/transfer-schedule.js';
9
12
  export { backupExtensions, type CaptureAndWaitOptions, type RestoreAndWaitOptions, restoreExtensions, transferExtensions, TransferFailedError, TransferTimeoutError, type WaitForTransferOptions, } from '../data/transfer/index.js';
@@ -2,8 +2,11 @@ export { databaseCredentialExtensions } from '../data/database-credential/index.
2
2
  export { databaseExtensions } from '../data/database.js';
3
3
  export { maintenanceExtensions } from '../data/maintenance.js';
4
4
  export { postgresDatabaseExtensions } from '../data/postgres-database.js';
5
+ export { isCancellable, isRunnable, MigrationInvalidError, MigrationNotCancellableError, MigrationNotRunnableError, postgresMigrationExtensions, } from '../data/postgres-migration.js';
6
+ export { LEADER_POOL_NAME, MAX_POOL_INSTANCES, postgresPoolExtensions, } from '../data/postgres-pool.js';
5
7
  export { postgresQuotaExtensions } from '../data/postgres-quota.js';
6
- export { postgresExtensions } from '../data/postgres.js';
8
+ export { postgresSettingsExtensions } from '../data/postgres-settings.js';
9
+ export { postgresExtensions, } from '../data/postgres.js';
7
10
  export { redisExtensions } from '../data/redis/index.js';
8
11
  export { transferScheduleExtensions } from '../data/transfer-schedule.js';
9
12
  export { backupExtensions, restoreExtensions, transferExtensions, TransferFailedError, TransferTimeoutError, } from '../data/transfer/index.js';
@@ -1,6 +1,5 @@
1
- import type { AddOn } from '@heroku/types/3.sdk';
2
1
  import type { ResourceCtx } from '../../../core/extend-resource.js';
3
- import type { DestroyAndWaitOptions } from './types.js';
2
+ import type { DeprovisioningAddOn, DestroyAndWaitOptions } from './types.js';
4
3
  /**
5
4
  * Delete an add-on and optionally wait for deprovisioning to complete.
6
5
  *
@@ -19,4 +18,4 @@ import type { DestroyAndWaitOptions } from './types.js';
19
18
  *
20
19
  * Both the delete and the poll requests send `Accept-Expansion: plan`.
21
20
  */
22
- export declare function destroyAndWait(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, addonIdentity: string, options?: DestroyAndWaitOptions): Promise<AddOn>;
21
+ export declare function destroyAndWait(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, addonIdentity: string, options?: DestroyAndWaitOptions): Promise<DeprovisioningAddOn>;
@@ -29,7 +29,13 @@ export async function destroyAndWait(ctx, appIdentity, addonIdentity, options =
29
29
  ? platform.withOptions({ body: { force: true } })
30
30
  : platform;
31
31
  const deleted = await deleteClient.addOn.delete(appIdentity, addonIdentity);
32
- let addon = deleted ?? {};
32
+ if (!deleted) {
33
+ // The platform returns the (possibly still-deprovisioning) add-on on a
34
+ // successful delete; an empty body means we can't report a result, so fail
35
+ // loudly rather than fabricate a stateless phantom add-on.
36
+ throw new Error(`Deleting add-on ${addonIdentity} returned no response body.`);
37
+ }
38
+ let addon = deleted;
33
39
  if (!options.wait || (addon.state !== 'deprovisioning' && addon.state !== 'provisioning')) {
34
40
  return addon;
35
41
  }
@@ -1,4 +1,5 @@
1
1
  import type { AddOn } from '@heroku/types/3.sdk';
2
+ import type { DeprovisioningAddOn, ResolvedAddOn } from './types.js';
2
3
  export declare class AddonNotFoundError extends Error {
3
4
  readonly resource: string;
4
5
  readonly id = "not_found";
@@ -36,14 +37,46 @@ export declare class AddonConfirmationRequiredError extends Error {
36
37
  message: string;
37
38
  };
38
39
  }
40
+ /**
41
+ * Thrown by `postgresDatabase.destroy` when the database can't be found under
42
+ * the requested app but does exist under another app. Restores the CLI's
43
+ * historical "is on <app> not <app>" guidance so a database scoped to the
44
+ * wrong app surfaces a targeted error rather than a bare not-found.
45
+ */
46
+ export declare class AddonAppMismatchError extends Error {
47
+ readonly addon: ResolvedAddOn;
48
+ readonly requestedApp: string;
49
+ readonly id = "app_mismatch";
50
+ constructor(addon: ResolvedAddOn, requestedApp: string);
51
+ get body(): {
52
+ id: string;
53
+ message: string;
54
+ };
55
+ }
56
+ /**
57
+ * Thrown by `postgresDatabase.destroy` when the delete request (or its
58
+ * deprovisioning wait) fails after the database was resolved and any
59
+ * `beforeDestroy` confirmation passed. Carries the resolved add-on so callers
60
+ * can render tier-specific guidance without re-resolving; the underlying
61
+ * failure is preserved as the standard `cause`.
62
+ */
63
+ export declare class AddonDestroyFailedError extends Error {
64
+ readonly addon: ResolvedAddOn;
65
+ readonly id = "destroy_failed";
66
+ constructor(addon: ResolvedAddOn, cause: unknown);
67
+ get body(): {
68
+ id: string;
69
+ message: string;
70
+ };
71
+ }
39
72
  /**
40
73
  * Thrown by `createAndWait` when the Platform reports the add-on
41
74
  * settled into a non-provisioned terminal state (e.g. `deprovisioned`).
42
75
  */
43
76
  export declare class AddonProvisioningFailedError extends Error {
44
- readonly addon: AddOn;
77
+ readonly addon: AddOn | DeprovisioningAddOn;
45
78
  readonly id = "provisioning_failed";
46
- constructor(addon: AddOn);
79
+ constructor(addon: AddOn | DeprovisioningAddOn);
47
80
  get body(): {
48
81
  id: string;
49
82
  message: string;
@@ -43,6 +43,58 @@ export class AddonConfirmationRequiredError extends Error {
43
43
  return { id: this.id, message: this.message };
44
44
  }
45
45
  }
46
+ /**
47
+ * Thrown by `postgresDatabase.destroy` when the database can't be found under
48
+ * the requested app but does exist under another app. Restores the CLI's
49
+ * historical "is on <app> not <app>" guidance so a database scoped to the
50
+ * wrong app surfaces a targeted error rather than a bare not-found.
51
+ */
52
+ export class AddonAppMismatchError extends Error {
53
+ addon;
54
+ requestedApp;
55
+ id = 'app_mismatch';
56
+ constructor(addon, requestedApp) {
57
+ super(`Database ${addon.name} is on ${addon.app.name} not ${requestedApp}. Try again with the correct app.`);
58
+ this.addon = addon;
59
+ this.requestedApp = requestedApp;
60
+ this.name = 'AddonAppMismatchError';
61
+ }
62
+ get body() {
63
+ return { id: this.id, message: this.message };
64
+ }
65
+ }
66
+ /**
67
+ * Thrown by `postgresDatabase.destroy` when the delete request (or its
68
+ * deprovisioning wait) fails after the database was resolved and any
69
+ * `beforeDestroy` confirmation passed. Carries the resolved add-on so callers
70
+ * can render tier-specific guidance without re-resolving; the underlying
71
+ * failure is preserved as the standard `cause`.
72
+ */
73
+ export class AddonDestroyFailedError extends Error {
74
+ addon;
75
+ id = 'destroy_failed';
76
+ constructor(addon, cause) {
77
+ super(messageFromCause(cause), { cause });
78
+ this.addon = addon;
79
+ this.name = 'AddonDestroyFailedError';
80
+ }
81
+ get body() {
82
+ return { id: this.id, message: this.message };
83
+ }
84
+ }
85
+ function messageFromCause(cause) {
86
+ if (typeof cause === 'object' && cause !== null) {
87
+ const { body } = cause;
88
+ if (body?.message) {
89
+ return body.message;
90
+ }
91
+ const { message } = cause;
92
+ if (message) {
93
+ return message;
94
+ }
95
+ }
96
+ return String(cause);
97
+ }
46
98
  /**
47
99
  * Thrown by `createAndWait` when the Platform reports the add-on
48
100
  * settled into a non-provisioned terminal state (e.g. `deprovisioned`).