@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.
@@ -0,0 +1,313 @@
1
+ import { NotFoundError } from '@heroku/heroku-fetch';
2
+ import { extendResource } from '../../core/extend-resource.js';
3
+ import { withSignal } from '../../core/with-signal.js';
4
+ import { isAdvancedDatabase, isEssentialDatabase, isLegacyDatabase, isPostgresAddon, } from './internal/postgres-utils.js';
5
+ /**
6
+ * Thrown by `create` when the migration inputs are invalid before any request
7
+ * fires. Carries a machine `reason` so callers can branch without string
8
+ * matching.
9
+ */
10
+ export class MigrationInvalidError extends Error {
11
+ reason;
12
+ id = 'migration_invalid';
13
+ constructor(reason, message) {
14
+ super(message);
15
+ this.reason = reason;
16
+ this.name = 'MigrationInvalidError';
17
+ }
18
+ get body() {
19
+ return { id: this.id, message: this.message, reason: this.reason };
20
+ }
21
+ }
22
+ /**
23
+ * Thrown by `run` when the migration isn't in a startable state. Carries the
24
+ * migration (and thus its `status`) so callers can report the drift without
25
+ * re-fetching.
26
+ */
27
+ export class MigrationNotRunnableError extends Error {
28
+ migration;
29
+ id = 'migration_not_runnable';
30
+ constructor(migration) {
31
+ super(`Migration ${migration.id} can't be started from status '${migration.status}'. Only migrations in the 'ready' status can be started.`);
32
+ this.migration = migration;
33
+ this.name = 'MigrationNotRunnableError';
34
+ }
35
+ get body() {
36
+ return { id: this.id, message: this.message, status: this.migrationStatus };
37
+ }
38
+ /**
39
+ * The migration's status. Named `migrationStatus` (not `status`) to avoid
40
+ * shadowing the HTTP-status convention other `Error` subclasses in the SDK
41
+ * carry.
42
+ */
43
+ get migrationStatus() {
44
+ return this.migration.status;
45
+ }
46
+ }
47
+ /**
48
+ * Thrown by `cancel` when the migration isn't in a cancellable state. Carries
49
+ * the migration (and thus its `status`).
50
+ */
51
+ export class MigrationNotCancellableError extends Error {
52
+ migration;
53
+ id = 'migration_not_cancellable';
54
+ constructor(migration) {
55
+ super(`Migration ${migration.id} can't be canceled from status '${migration.status}'. Only migrations in the 'ready' status can be canceled.`);
56
+ this.migration = migration;
57
+ this.name = 'MigrationNotCancellableError';
58
+ }
59
+ get body() {
60
+ return { id: this.id, message: this.message, status: this.migrationStatus };
61
+ }
62
+ /**
63
+ * The migration's status. Named `migrationStatus` (not `status`) to avoid
64
+ * shadowing the HTTP-status convention other `Error` subclasses in the SDK
65
+ * carry.
66
+ */
67
+ get migrationStatus() {
68
+ return this.migration.status;
69
+ }
70
+ }
71
+ /**
72
+ * Compile-time exhaustiveness guard with a safe runtime fallback. A newly
73
+ * generated `MigrationStatus` that isn't handled above is a type error here
74
+ * (the `never` parameter), but if one reaches this at runtime — e.g. the API
75
+ * returns a status the type doesn't yet know about — we return `fallback`
76
+ * rather than throwing, so `run`/`cancel` fail closed (treat it as not
77
+ * runnable/cancellable) instead of crashing.
78
+ */
79
+ function exhaustiveFallback(_status, fallback) {
80
+ return fallback;
81
+ }
82
+ /**
83
+ * Whether a migration can be started. Only `ready` migrations are runnable,
84
+ * matching the CLI's menu gate. The exhaustive `switch` makes a newly
85
+ * generated status a compile error until its policy is decided here, rather
86
+ * than silently defaulting to runnable or not.
87
+ */
88
+ export function isRunnable(status) {
89
+ switch (status) {
90
+ case 'cancelled':
91
+ case 'completed':
92
+ case 'creating_target':
93
+ case 'failed':
94
+ case 'migrating':
95
+ case 'preparing':
96
+ case 'promoting':
97
+ case 'unknown': {
98
+ return false;
99
+ }
100
+ case 'ready': {
101
+ return true;
102
+ }
103
+ default: {
104
+ return exhaustiveFallback(status, false);
105
+ }
106
+ }
107
+ }
108
+ /**
109
+ * Whether a migration can be canceled. Only `ready` migrations are
110
+ * cancellable, matching the CLI's menu gate. Exhaustive for the same reason as
111
+ * {@link isRunnable}.
112
+ */
113
+ export function isCancellable(status) {
114
+ switch (status) {
115
+ case 'cancelled':
116
+ case 'completed':
117
+ case 'creating_target':
118
+ case 'failed':
119
+ case 'migrating':
120
+ case 'preparing':
121
+ case 'promoting':
122
+ case 'unknown': {
123
+ return false;
124
+ }
125
+ case 'ready': {
126
+ return true;
127
+ }
128
+ default: {
129
+ return exhaustiveFallback(status, false);
130
+ }
131
+ }
132
+ }
133
+ /**
134
+ * Fetches the migration currently targeting `targetAddonId`.
135
+ *
136
+ * The data API's migration endpoint is a single-object `GET` (not a list);
137
+ * when no migration exists it 404s, which this surfaces as `undefined` rather
138
+ * than throwing, so callers can treat "no migration" as a normal state.
139
+ */
140
+ export async function info(ctx, targetAddonId, options = {}) {
141
+ const { signal } = options;
142
+ signal?.throwIfAborted();
143
+ try {
144
+ return await withSignal(ctx.data, signal).postgresMigration.info(targetAddonId);
145
+ }
146
+ catch (error) {
147
+ if (error instanceof NotFoundError)
148
+ return undefined;
149
+ throw error;
150
+ }
151
+ }
152
+ /**
153
+ * Configures a new migration *to* `targetAddonId` *from* the database named in
154
+ * `body.source_id`.
155
+ *
156
+ * The URL identifies the **target**; the body's `source_id` identifies the
157
+ * **source** — both are add-on ids, so the two are easy to swap. A
158
+ * client-side guard rejects a source that equals the target before any request
159
+ * fires.
160
+ */
161
+ export async function create(ctx, targetAddonId, body, options = {}) {
162
+ const { signal } = options;
163
+ signal?.throwIfAborted();
164
+ if (body.source_id === targetAddonId) {
165
+ throw new MigrationInvalidError('source_equals_target', `A migration's source and target must differ, but both are '${targetAddonId}'.`);
166
+ }
167
+ return withSignal(ctx.data, signal).postgresMigration.create(targetAddonId, body);
168
+ }
169
+ /**
170
+ * Best-effort re-fetch of a migration after a successful `run`/`cancel` POST so
171
+ * the returned status reflects the transition the action kicked off.
172
+ *
173
+ * The POST already succeeded by the time this runs, so the re-fetch must never
174
+ * turn a completed action into a thrown error: the caller's `signal` is
175
+ * deliberately *not* threaded in (a post-POST abort shouldn't surface as a
176
+ * failure of an action that already happened), and any re-fetch error is
177
+ * swallowed. When the migration can't be re-read — it 404s (already gone) or
178
+ * the re-fetch fails for any other reason — we fall back to the gated record
179
+ * with its status forced to `'unknown'` rather than returning the stale
180
+ * `'ready'` the gate observed, which would wrongly imply the action never ran.
181
+ */
182
+ async function refetchAfterAction(ctx, targetAddonId, gated) {
183
+ try {
184
+ const refreshed = await info(ctx, targetAddonId);
185
+ return refreshed ?? { ...gated, status: 'unknown' };
186
+ }
187
+ catch {
188
+ return { ...gated, status: 'unknown' };
189
+ }
190
+ }
191
+ /**
192
+ * Starts the migration targeting `targetAddonId`.
193
+ *
194
+ * Fetches the migration first and gates on its status — a defensive backstop
195
+ * against state drift between listing and action: throws
196
+ * `MigrationNotRunnableError` (carrying the migration) if it's no longer
197
+ * `ready`, and lets a `NotFoundError` propagate if the migration is already
198
+ * gone. Once the run POST succeeds, returns the migration's post-action state
199
+ * via a best-effort re-fetch (see {@link refetchAfterAction}); never throws
200
+ * after the action has fired.
201
+ */
202
+ export async function run(ctx, targetAddonId, options = {}) {
203
+ const { signal } = options;
204
+ signal?.throwIfAborted();
205
+ const data = withSignal(ctx.data, signal);
206
+ const migration = await data.postgresMigration.info(targetAddonId);
207
+ if (!isRunnable(migration.status)) {
208
+ throw new MigrationNotRunnableError(migration);
209
+ }
210
+ signal?.throwIfAborted();
211
+ await data.postgresMigration.run(targetAddonId);
212
+ return refetchAfterAction(ctx, targetAddonId, migration);
213
+ }
214
+ /**
215
+ * Cancels the migration targeting `targetAddonId`.
216
+ *
217
+ * Gates on status like {@link run}: throws `MigrationNotCancellableError`
218
+ * (carrying the migration) if it's not `ready`, and lets a `NotFoundError`
219
+ * propagate if the migration is already gone. Once the cancel POST succeeds,
220
+ * returns the migration's post-action state via a best-effort re-fetch (see
221
+ * {@link refetchAfterAction}); never throws after the action has fired.
222
+ */
223
+ export async function cancel(ctx, targetAddonId, options = {}) {
224
+ const { signal } = options;
225
+ signal?.throwIfAborted();
226
+ const data = withSignal(ctx.data, signal);
227
+ const migration = await data.postgresMigration.info(targetAddonId);
228
+ if (!isCancellable(migration.status)) {
229
+ throw new MigrationNotCancellableError(migration);
230
+ }
231
+ signal?.throwIfAborted();
232
+ await data.postgresMigration.cancel(targetAddonId);
233
+ return refetchAfterAction(ctx, targetAddonId, migration);
234
+ }
235
+ /**
236
+ * Lists everything the migration UI needs for an app in one pass: the classic
237
+ * databases that can be sources, the Advanced databases that can be targets
238
+ * (each with its `info` and any migration targeting it), and the flat list of
239
+ * configured migrations.
240
+ *
241
+ * Replaces the CLI's `getAppDatabases` + `getMigrationTargetsAndInfo`: resolves
242
+ * the app's add-on attachments (with `addon:plan,config_vars` inclusions so
243
+ * plans are present for tier classification), keeps only Postgres databases
244
+ * the app owns, dedupes to unique add-ons with their attachment names, then
245
+ * fans out `/migrations` and `/info` per Advanced database. A per-database 404
246
+ * is tolerated (that database simply isn't a migration target yet); the first
247
+ * non-404 rejection is rethrown.
248
+ */
249
+ export async function listMigrationContext(ctx, appIdentity, options = {}) {
250
+ const { signal } = options;
251
+ signal?.throwIfAborted();
252
+ const platform = withSignal(ctx.platform.withHeaders({ 'Accept-Inclusion': 'addon:plan,config_vars' }), signal);
253
+ const data = withSignal(ctx.data, signal);
254
+ const attachments = await platform.addOnAttachment.listByApp(appIdentity);
255
+ // Keep only Postgres databases this app *owns* (attachments to databases
256
+ // owned by other apps are excluded), then dedupe to unique add-ons while
257
+ // collecting the names each is attached under (insertion order preserved).
258
+ const byAddonId = new Map();
259
+ for (const attachment of attachments) {
260
+ const { addon } = attachment;
261
+ if (!isPostgresAddon(addon) || (addon.app?.name !== appIdentity && addon.app?.id !== appIdentity))
262
+ continue;
263
+ const existing = byAddonId.get(addon.id);
264
+ if (existing) {
265
+ existing.attachmentNames.push(attachment.name);
266
+ }
267
+ else {
268
+ byAddonId.set(addon.id, { ...addon, attachmentNames: [attachment.name] });
269
+ }
270
+ }
271
+ const databases = [...byAddonId.values()];
272
+ const sources = databases.filter(db => isLegacyDatabase(db) && !isEssentialDatabase(db));
273
+ const advanced = databases.filter(db => isAdvancedDatabase(db));
274
+ const [migrationResults, infoResults] = await Promise.all([
275
+ Promise.allSettled(advanced.map(db => data.postgresMigration.info(db.id))),
276
+ Promise.allSettled(advanced.map(db => data.postgres.info(db.id))),
277
+ ]);
278
+ // A 404 means the database isn't a migration target yet (or has no info) —
279
+ // expected. Anything else is a real failure; rethrow the first one.
280
+ const unexpected = [...migrationResults, ...infoResults].find((result) => result.status === 'rejected' && !(result.reason instanceof NotFoundError));
281
+ if (unexpected)
282
+ throw unexpected.reason;
283
+ const targets = advanced.map((db, index) => {
284
+ const migrationResult = migrationResults[index];
285
+ const infoResult = infoResults[index];
286
+ return {
287
+ ...db,
288
+ info: infoResult.status === 'fulfilled' ? infoResult.value : undefined,
289
+ migration: migrationResult.status === 'fulfilled' ? migrationResult.value : undefined,
290
+ };
291
+ });
292
+ const migrations = targets
293
+ .map(target => target.migration)
294
+ .filter((migration) => migration !== undefined);
295
+ return { migrations, sources, targets };
296
+ }
297
+ // These extension keys are intentionally named distinctly from the generated
298
+ // `postgresMigration` routes (`info`/`create`/`run`/`cancel`) they wrap.
299
+ // Registering an extension under a generated route's own name silently replaces
300
+ // that route (the replacement is only debug-logged), which both hides the
301
+ // orchestration behind a familiar-looking name and leaves no way to reach the
302
+ // raw route. The verbs here also describe the guarded behavior the raw routes
303
+ // lack: `find` returns undefined instead of throwing on 404; `prepare`
304
+ // configures (and validates) a migration without starting it; `startWhenReady`
305
+ // and `abortBeforeRun` gate on the migration still being in its pre-run `ready`
306
+ // state before acting, then re-fetch the post-action status.
307
+ export const postgresMigrationExtensions = extendResource('data', 'postgresMigration', ctx => ({
308
+ abortBeforeRun: (targetAddonId, options) => cancel(ctx, targetAddonId, options),
309
+ find: (targetAddonId, options) => info(ctx, targetAddonId, options),
310
+ listMigrationContext: (appIdentity, options) => listMigrationContext(ctx, appIdentity, options),
311
+ prepare: (targetAddonId, body, options) => create(ctx, targetAddonId, body, options),
312
+ startWhenReady: (targetAddonId, options) => run(ctx, targetAddonId, options),
313
+ }));
@@ -0,0 +1,77 @@
1
+ import type { PostgresLevelInfoResult, PostgresPoolInfoResult } from '@heroku/types/data';
2
+ /** The maximum number of follower-pool instances a database cluster can hold. */
3
+ export declare const MAX_POOL_INSTANCES = 13;
4
+ /** The name the leader pool always carries; any other name is a follower pool. */
5
+ export declare const LEADER_POOL_NAME = "leader";
6
+ /** A single compute level from `postgresLevel.info`. */
7
+ export type LevelItem = PostgresLevelInfoResult['items'][number];
8
+ /**
9
+ * A level paired with whether it's selectable for a given pool. The level a
10
+ * pool is already at is reported unavailable (`reason: 'current_level'`),
11
+ * mirroring the CLI's "current level" disabling; every other level is
12
+ * available. Callers get the full `LevelItem` (vcpu/memory/connection_limit),
13
+ * not just a name, so they can render without a second lookup.
14
+ */
15
+ export type AvailableLevel = {
16
+ level: LevelItem;
17
+ } & ({
18
+ available: false;
19
+ reason: 'current_level';
20
+ } | {
21
+ available: true;
22
+ });
23
+ /**
24
+ * An action that can be taken on a pool. A discriminated union on `type`:
25
+ * `change_level` applies to any pool; `add_ha`/`remove_ha` are the leader's
26
+ * HA toggle (carrying the instance `count` the mutation sets); `update_count`
27
+ * (carrying the selectable `min`/`max`) and `destroy` apply to follower pools.
28
+ */
29
+ export type PoolAction = {
30
+ count: 1;
31
+ type: 'remove_ha';
32
+ } | {
33
+ count: 2;
34
+ type: 'add_ha';
35
+ } | {
36
+ max: number;
37
+ min: number;
38
+ type: 'update_count';
39
+ } | {
40
+ type: 'change_level';
41
+ } | {
42
+ type: 'destroy';
43
+ };
44
+ export type ListAvailableActionsOptions = {
45
+ /**
46
+ * Total instances across *all* follower pools in the cluster, *including*
47
+ * the pool being queried. Used to bound a follower's `update_count` range:
48
+ * the edited pool's own instances are excluded so it can grow back into the
49
+ * budget they occupy (`max = MAX_POOL_INSTANCES - (count - pool.expected_count)`,
50
+ * floored at 1). Required so an omitted count can't silently widen the range
51
+ * to the full budget. Ignored for the leader pool.
52
+ */
53
+ followerInstanceCount: number;
54
+ };
55
+ /**
56
+ * Derives the selectable compute levels for a pool. Pure — no request.
57
+ *
58
+ * Returns every level from `levels`, marking the pool's current
59
+ * `expected_level` unavailable (`reason: 'current_level'`) so the UI can
60
+ * disable it. With no `pool`, every level is available (e.g. choosing the
61
+ * level for a brand-new pool).
62
+ */
63
+ export declare function listAvailableLevels(levels: PostgresLevelInfoResult, pool?: Pick<PostgresPoolInfoResult, 'expected_level'>): AvailableLevel[];
64
+ /**
65
+ * Derives the actions available on a pool. Pure — no request.
66
+ *
67
+ * The leader pool can always change level and can toggle high availability:
68
+ * `remove_ha` (down to a single instance) when it currently runs a standby
69
+ * (`expected_count > 1`), otherwise `add_ha` (up to two instances). A follower
70
+ * pool can change level, update its instance count (bounded by the cluster's
71
+ * remaining instance budget), or be destroyed.
72
+ */
73
+ export declare function listAvailableActions(pool: Pick<PostgresPoolInfoResult, 'expected_count' | 'name'>, options: ListAvailableActionsOptions): PoolAction[];
74
+ export declare const postgresPoolExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "postgresPool", {
75
+ listAvailableActions: typeof listAvailableActions;
76
+ listAvailableLevels: typeof listAvailableLevels;
77
+ }>;
@@ -0,0 +1,50 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ /** The maximum number of follower-pool instances a database cluster can hold. */
3
+ export const MAX_POOL_INSTANCES = 13;
4
+ /** The name the leader pool always carries; any other name is a follower pool. */
5
+ export const LEADER_POOL_NAME = 'leader';
6
+ /**
7
+ * Derives the selectable compute levels for a pool. Pure — no request.
8
+ *
9
+ * Returns every level from `levels`, marking the pool's current
10
+ * `expected_level` unavailable (`reason: 'current_level'`) so the UI can
11
+ * disable it. With no `pool`, every level is available (e.g. choosing the
12
+ * level for a brand-new pool).
13
+ */
14
+ export function listAvailableLevels(levels, pool) {
15
+ return levels.items.map(level => level.name === pool?.expected_level
16
+ ? { available: false, level, reason: 'current_level' }
17
+ : { available: true, level });
18
+ }
19
+ /**
20
+ * Derives the actions available on a pool. Pure — no request.
21
+ *
22
+ * The leader pool can always change level and can toggle high availability:
23
+ * `remove_ha` (down to a single instance) when it currently runs a standby
24
+ * (`expected_count > 1`), otherwise `add_ha` (up to two instances). A follower
25
+ * pool can change level, update its instance count (bounded by the cluster's
26
+ * remaining instance budget), or be destroyed.
27
+ */
28
+ export function listAvailableActions(pool, options) {
29
+ if (pool.name === LEADER_POOL_NAME) {
30
+ return [
31
+ { type: 'change_level' },
32
+ pool.expected_count > 1 ? { count: 1, type: 'remove_ha' } : { count: 2, type: 'add_ha' },
33
+ ];
34
+ }
35
+ const { followerInstanceCount } = options;
36
+ // `followerInstanceCount` includes this pool's own instances; exclude them so
37
+ // the pool can grow back into the budget it already occupies. Floor at 1 so
38
+ // the range never collapses below `min` even if the counts are inconsistent.
39
+ const otherInstances = Math.max(0, followerInstanceCount - pool.expected_count);
40
+ const max = Math.max(1, MAX_POOL_INSTANCES - otherInstances);
41
+ return [
42
+ { type: 'change_level' },
43
+ { max, min: 1, type: 'update_count' },
44
+ { type: 'destroy' },
45
+ ];
46
+ }
47
+ export const postgresPoolExtensions = extendResource('data', 'postgresPool', () => ({
48
+ listAvailableActions,
49
+ listAvailableLevels,
50
+ }));
@@ -0,0 +1,33 @@
1
+ import type { PostgresSettingsInfoResult, PostgresSettingsUpdateOpts, PostgresSettingsUpdateResult } from '@heroku/types/data';
2
+ import type { ResourceCtx } from '../../core/extend-resource.js';
3
+ import type { ResolvedAddOn } from '../platform/add-on/types.js';
4
+ export type PostgresSettingsOptions = {
5
+ appIdentity?: string;
6
+ signal?: AbortSignal;
7
+ };
8
+ export type GetPostgresSettingsResult = {
9
+ addon: ResolvedAddOn;
10
+ settings: PostgresSettingsInfoResult;
11
+ };
12
+ export type UpdatePostgresSettingsResult = {
13
+ addon: ResolvedAddOn;
14
+ settings: PostgresSettingsUpdateResult;
15
+ };
16
+ /**
17
+ * Reads the settings of an Advanced-tier Postgres database.
18
+ *
19
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved addon) if
20
+ * `addonIdentity` doesn't resolve to an Advanced-tier database.
21
+ */
22
+ export declare function info(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addonIdentity: string, options?: PostgresSettingsOptions): Promise<GetPostgresSettingsResult>;
23
+ /**
24
+ * Updates the settings of an Advanced-tier Postgres database.
25
+ *
26
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved addon) if
27
+ * `addonIdentity` doesn't resolve to an Advanced-tier database.
28
+ */
29
+ export declare function update(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addonIdentity: string, body: PostgresSettingsUpdateOpts, options?: PostgresSettingsOptions): Promise<UpdatePostgresSettingsResult>;
30
+ export declare const postgresSettingsExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "postgresSettings", {
31
+ info: (addonIdentity: string, options?: PostgresSettingsOptions) => Promise<GetPostgresSettingsResult>;
32
+ update: (addonIdentity: string, body: PostgresSettingsUpdateOpts, options?: PostgresSettingsOptions) => Promise<UpdatePostgresSettingsResult>;
33
+ }>;
@@ -0,0 +1,47 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ import { withSignal } from '../../core/with-signal.js';
3
+ import { resolveAddonInternal } from '../platform/add-on/resolve.js';
4
+ import { getPostgresServiceName, isAdvancedDatabase, NotAdvancedDatabaseError } from './internal/postgres-utils.js';
5
+ async function resolveTieredAddon(ctx, addonIdentity, options) {
6
+ const { appIdentity, signal } = options;
7
+ signal?.throwIfAborted();
8
+ // Bind the signal so the resolve request aborts in flight.
9
+ const platform = withSignal(ctx.platform, signal);
10
+ const addon = await resolveAddonInternal(platform, addonIdentity, {
11
+ addonService: getPostgresServiceName(), appIdentity,
12
+ });
13
+ if (!isAdvancedDatabase(addon)) {
14
+ throw new NotAdvancedDatabaseError(addon, `You can only manage settings on Advanced-tier databases. ${addon.name} isn't an Advanced-tier database.`);
15
+ }
16
+ return addon;
17
+ }
18
+ /**
19
+ * Reads the settings of an Advanced-tier Postgres database.
20
+ *
21
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved addon) if
22
+ * `addonIdentity` doesn't resolve to an Advanced-tier database.
23
+ */
24
+ export async function info(ctx, addonIdentity, options = {}) {
25
+ const addon = await resolveTieredAddon(ctx, addonIdentity, options);
26
+ // Bind the signal so the info request aborts in flight.
27
+ const data = withSignal(ctx.data, options.signal);
28
+ const settings = await data.postgresSettings.info(addon.id);
29
+ return { addon, settings };
30
+ }
31
+ /**
32
+ * Updates the settings of an Advanced-tier Postgres database.
33
+ *
34
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved addon) if
35
+ * `addonIdentity` doesn't resolve to an Advanced-tier database.
36
+ */
37
+ export async function update(ctx, addonIdentity, body, options = {}) {
38
+ const addon = await resolveTieredAddon(ctx, addonIdentity, options);
39
+ // Bind the signal so the update request aborts in flight.
40
+ const data = withSignal(ctx.data, options.signal);
41
+ const settings = await data.postgresSettings.update(addon.id, body);
42
+ return { addon, settings };
43
+ }
44
+ export const postgresSettingsExtensions = extendResource('data', 'postgresSettings', ctx => ({
45
+ info: (addonIdentity, options) => info(ctx, addonIdentity, options),
46
+ update: (addonIdentity, body, options) => update(ctx, addonIdentity, body, options),
47
+ }));