@heroku/sdk 0.12.2 → 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.
Files changed (33) hide show
  1. package/dist/resources/data/database-credential/index.d.ts +0 -1
  2. package/dist/resources/data/database-credential/index.js +0 -1
  3. package/dist/resources/data/database-credential/list.d.ts +0 -15
  4. package/dist/resources/data/database-credential/list.js +2 -20
  5. package/dist/resources/data/database-credential/rotate.d.ts +1 -3
  6. package/dist/resources/data/internal/postgres-utils.d.ts +27 -11
  7. package/dist/resources/data/internal/postgres-utils.js +19 -7
  8. package/dist/resources/data/internal/resolve-pg-database.js +2 -2
  9. package/dist/resources/data/postgres-database.d.ts +107 -1
  10. package/dist/resources/data/postgres-database.js +207 -1
  11. package/dist/resources/data/postgres-migration.d.ts +182 -0
  12. package/dist/resources/data/postgres-migration.js +313 -0
  13. package/dist/resources/data/postgres-pool.d.ts +77 -0
  14. package/dist/resources/data/postgres-pool.js +50 -0
  15. package/dist/resources/data/postgres-quota.d.ts +45 -0
  16. package/dist/resources/data/postgres-quota.js +61 -0
  17. package/dist/resources/data/postgres-settings.d.ts +33 -0
  18. package/dist/resources/data/postgres-settings.js +47 -0
  19. package/dist/resources/data/postgres.d.ts +229 -0
  20. package/dist/resources/data/postgres.js +273 -0
  21. package/dist/resources/data/redis/index.d.ts +1 -1
  22. package/dist/resources/extensions/data.d.ts +5 -0
  23. package/dist/resources/extensions/data.js +5 -0
  24. package/dist/resources/platform/add-on/destroy-and-wait.d.ts +2 -3
  25. package/dist/resources/platform/add-on/destroy-and-wait.js +7 -1
  26. package/dist/resources/platform/add-on/errors.d.ts +35 -2
  27. package/dist/resources/platform/add-on/errors.js +52 -0
  28. package/dist/resources/platform/add-on/index.d.ts +3 -3
  29. package/dist/resources/platform/add-on/index.js +1 -1
  30. package/dist/resources/platform/add-on/types.d.ts +14 -1
  31. package/dist/version.d.ts +1 -1
  32. package/dist/version.js +1 -1
  33. package/package.json +1 -1
@@ -7,7 +7,6 @@ export type DatabaseCredentialOptions = {
7
7
  export { create, type CreateDatabaseCredentialResult } from './create.js';
8
8
  export { destroy, type DestroyDatabaseCredentialResult } from './destroy.js';
9
9
  export { type GetDatabaseCredentialUrlResult, getUrl, } from './get-url.js';
10
- export { NotAdvancedDatabaseError } from './list.js';
11
10
  export { list, type ListDatabaseCredentialsResult } from './list.js';
12
11
  export { rotate, type RotateAllDatabaseCredentialsResult, type RotateDatabaseCredentialOptions, type RotateDatabaseCredentialOpts, type RotateDatabaseCredentialPreview, type RotateDatabaseCredentialResult, } from './rotate.js';
13
12
  export declare const databaseCredentialExtensions: import("../../../core/extend-resource.js").ResourceExtension<"data", "databaseCredential", {
@@ -7,7 +7,6 @@ import { rotate } from './rotate.js';
7
7
  export { create } from './create.js';
8
8
  export { destroy } from './destroy.js';
9
9
  export { getUrl, } from './get-url.js';
10
- export { NotAdvancedDatabaseError } from './list.js';
11
10
  export { list } from './list.js';
12
11
  export { rotate, } from './rotate.js';
13
12
  export const databaseCredentialExtensions = extendResource('data', 'databaseCredential', ctx => {
@@ -2,21 +2,6 @@ import type { PostgresCredentialListResult } from '@heroku/types/data';
2
2
  import type { ResourceCtx } from '../../../core/extend-resource.js';
3
3
  import type { ResolvedAddOn } from '../../platform/add-on/types.js';
4
4
  import { DatabaseCredentialOptions } from './index.js';
5
- /**
6
- * Thrown by `list` when the resolved add-on is not an Advanced-tier
7
- * database. Carries the resolved addon so callers that need its identity
8
- * (e.g. to point the user at the classic-tier equivalent) don't need a
9
- * second resolve call.
10
- */
11
- export declare class NotAdvancedDatabaseError extends Error {
12
- readonly addon: ResolvedAddOn;
13
- readonly id = "not_advanced_database";
14
- constructor(addon: ResolvedAddOn);
15
- get body(): {
16
- id: string;
17
- message: string;
18
- };
19
- }
20
5
  export type ListDatabaseCredentialsResult = {
21
6
  addon: ResolvedAddOn;
22
7
  credentials: PostgresCredentialListResult;
@@ -1,24 +1,6 @@
1
1
  import { withSignal } from '../../../core/with-signal.js';
2
2
  import { resolveAddonInternal } from '../../platform/add-on/resolve.js';
3
- import { getPostgresServiceName, isAdvancedDatabase } from '../internal/postgres-utils.js';
4
- /**
5
- * Thrown by `list` when the resolved add-on is not an Advanced-tier
6
- * database. Carries the resolved addon so callers that need its identity
7
- * (e.g. to point the user at the classic-tier equivalent) don't need a
8
- * second resolve call.
9
- */
10
- export class NotAdvancedDatabaseError extends Error {
11
- addon;
12
- id = 'not_advanced_database';
13
- constructor(addon) {
14
- super(`You can only list credentials on Advanced-tier databases. ${addon.name} isn't an Advanced-tier database.`);
15
- this.addon = addon;
16
- this.name = 'NotAdvancedDatabaseError';
17
- }
18
- get body() {
19
- return { id: this.id, message: this.message };
20
- }
21
- }
3
+ import { getPostgresServiceName, isAdvancedDatabase, NotAdvancedDatabaseError } from '../internal/postgres-utils.js';
22
4
  /**
23
5
  * Lists credentials on an Advanced-tier Postgres database. Classic-tier
24
6
  * databases don't support this endpoint; use `postgresDatabase.listCredentials`
@@ -36,7 +18,7 @@ export async function list(ctx, addonIdentity, options = {}) {
36
18
  addonService: getPostgresServiceName(), appIdentity,
37
19
  });
38
20
  if (!isAdvancedDatabase(addon)) {
39
- throw new NotAdvancedDatabaseError(addon);
21
+ throw new NotAdvancedDatabaseError(addon, `You can only list credentials on Advanced-tier databases. ${addon.name} isn't an Advanced-tier database.`);
40
22
  }
41
23
  const data = withSignal(ctx.data, signal);
42
24
  const credentials = await data.postgresCredential.list(addon.id);
@@ -1,9 +1,8 @@
1
- import type { PostgresDatabaseInfoCredentialResult as NonAdvancedCredentialInfo, PostgresCredentialListResult, PostgresDatabaseRotateCredentialResult, PostgresDatabaseRotateCredentialsResult, PostgresRotateCredentialsResult } from '@heroku/types/data';
1
+ import type { PostgresCredentialInfoResult as AdvancedCredentialInfo, PostgresDatabaseInfoCredentialResult as NonAdvancedCredentialInfo, PostgresDatabaseRotateCredentialResult, PostgresDatabaseRotateCredentialsResult, PostgresRotateCredentialsResult } from '@heroku/types/data';
2
2
  import { AddOnAttachment } from '@heroku/types/3.sdk';
3
3
  import type { ResourceCtx } from '../../../core/extend-resource.js';
4
4
  import type { ResolvedAddOn } from '../../platform/add-on/types.js';
5
5
  import type { DatabaseCredentialOptions } from './index.js';
6
- type AdvancedCredentialInfo = PostgresCredentialListResult['items'][number];
7
6
  export type RotateDatabaseCredentialOptions = DatabaseCredentialOptions & {
8
7
  /**
9
8
  * Fires before the rotate request, allowing the caller to perform any
@@ -53,4 +52,3 @@ export declare function rotate(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addo
53
52
  all: true;
54
53
  }, options?: RotateDatabaseCredentialOptions): Promise<RotateAllDatabaseCredentialsResult>;
55
54
  export declare function rotate(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addonIdentity: string, body?: RotateDatabaseCredentialOpts, options?: RotateDatabaseCredentialOptions): Promise<RotateDatabaseCredentialResult>;
56
- export {};
@@ -1,21 +1,37 @@
1
1
  import type { AddOn } from '@heroku/types/3.sdk';
2
2
  import { PostgresCredentialInfoResult as AdvancedCredentialInfo } from '@heroku/types/data';
3
3
  import type { ResourceCtx } from '../../../core/extend-resource.js';
4
- import type { AddOnAttachmentWithInclusions } from '../../platform/add-on-attachment/resolve.js';
4
+ import type { ResolvedAddOnAttachment } from '../../platform/add-on-attachment/resolve.js';
5
5
  import { CredentialInfo as NonAdvancedCredentialInfo } from '../postgres-database.js';
6
+ /**
7
+ * The add-on shapes the Postgres tier predicates accept: either a full
8
+ * platform `AddOn` or the lighter add-on carried on a resolved attachment.
9
+ * Both expose the `plan.name` these checks read, so they're interchangeable
10
+ * here.
11
+ */
12
+ export type PostgresAddonLike = AddOn | ResolvedAddOnAttachment['addon'];
13
+ /**
14
+ * Thrown when an add-on is not an Advanced-tier database. Carries the add-on
15
+ * so callers that need its identity don't need a second resolve call.
16
+ */
17
+ export declare class NotAdvancedDatabaseError extends Error {
18
+ readonly addon: PostgresAddonLike;
19
+ readonly id = "not_advanced_database";
20
+ constructor(addon: PostgresAddonLike, message: string);
21
+ get body(): {
22
+ id: string;
23
+ message: string;
24
+ };
25
+ }
6
26
  /**
7
27
  * Returns the first legacy database add-on on the app.
8
28
  */
9
29
  export declare function findLegacyDatabase(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string): Promise<AddOn>;
10
30
  export declare function isAdvancedCredential(credential: AdvancedCredentialInfo | NonAdvancedCredentialInfo): credential is AdvancedCredentialInfo;
11
31
  export declare const getPostgresServiceName: () => string;
12
- /**
13
- * `AddOnAttachmentWithInclusions` is an internal type. The public `isPostgresAddon` takes `AddOn` only.
14
- */
15
- export declare const isPostgresAddonInternal: (addon: AddOn | AddOnAttachmentWithInclusions["addon"]) => boolean;
16
- export declare const isPostgresAddon: (addon: AddOn) => boolean;
17
- export declare const isAdvancedDatabase: (addon: AddOn) => boolean;
18
- export declare const isLegacyDatabase: (addon: AddOn) => boolean;
19
- export declare const isLegacyEssentialDatabase: (addon: AddOn) => boolean;
20
- export declare const isEssentialDatabase: (addon: AddOn) => boolean;
21
- export declare const isEssentialTier: (addon: AddOn) => boolean;
32
+ export declare const isPostgresAddon: (addon: PostgresAddonLike) => boolean;
33
+ export declare const isAdvancedDatabase: (addon: PostgresAddonLike) => boolean;
34
+ export declare const isLegacyDatabase: (addon: PostgresAddonLike) => boolean;
35
+ export declare const isLegacyEssentialDatabase: (addon: PostgresAddonLike) => boolean;
36
+ export declare const isEssentialDatabase: (addon: PostgresAddonLike) => boolean;
37
+ export declare const isEssentialTier: (addon: PostgresAddonLike) => boolean;
@@ -1,5 +1,21 @@
1
1
  import createDebug from 'debug';
2
2
  const debug = createDebug('heroku:sdk:resources:postgres-database');
3
+ /**
4
+ * Thrown when an add-on is not an Advanced-tier database. Carries the add-on
5
+ * so callers that need its identity don't need a second resolve call.
6
+ */
7
+ export class NotAdvancedDatabaseError extends Error {
8
+ addon;
9
+ id = 'not_advanced_database';
10
+ constructor(addon, message) {
11
+ super(message);
12
+ this.addon = addon;
13
+ this.name = 'NotAdvancedDatabaseError';
14
+ }
15
+ get body() {
16
+ return { id: this.id, message: this.message };
17
+ }
18
+ }
3
19
  /**
4
20
  * Returns the first legacy database add-on on the app.
5
21
  */
@@ -15,13 +31,9 @@ export function isAdvancedCredential(credential) {
15
31
  return 'roles' in credential;
16
32
  }
17
33
  export const getPostgresServiceName = () => process.env.HEROKU_POSTGRESQL_ADDON_NAME || process.env.HEROKU_DATA_SERVICE || 'heroku-postgresql';
18
- /**
19
- * `AddOnAttachmentWithInclusions` is an internal type. The public `isPostgresAddon` takes `AddOn` only.
20
- */
21
- export const isPostgresAddonInternal = (addon) => addon.plan?.name?.split(':', 2)[0] === getPostgresServiceName();
22
- export const isPostgresAddon = (addon) => isPostgresAddonInternal(addon);
23
- export const isAdvancedDatabase = (addon) => isPostgresAddonInternal(addon) && /^(advanced|performance)/.test(addon.plan?.name?.split(':', 2)[1] ?? '');
24
- export const isLegacyDatabase = (addon) => isPostgresAddonInternal(addon) && !isAdvancedDatabase(addon);
34
+ export const isPostgresAddon = (addon) => addon.plan?.name?.split(':', 2)[0] === getPostgresServiceName();
35
+ export const isAdvancedDatabase = (addon) => isPostgresAddon(addon) && /^(advanced|performance)/.test(addon.plan?.name?.split(':', 2)[1] ?? '');
36
+ export const isLegacyDatabase = (addon) => isPostgresAddon(addon) && !isAdvancedDatabase(addon);
25
37
  export const isLegacyEssentialDatabase = (addon) => isLegacyDatabase(addon) && /^(dev|basic|mini)/.test(addon.plan?.name?.split(':', 2)[1] ?? '');
26
38
  export const isEssentialDatabase = (addon) => isLegacyDatabase(addon) && (addon.plan?.name?.split(':', 2)[1] ?? '').startsWith('essential');
27
39
  export const isEssentialTier = (addon) => isEssentialDatabase(addon) || isLegacyEssentialDatabase(addon);
@@ -2,7 +2,7 @@ import { NotFoundError } from '@heroku/heroku-fetch';
2
2
  import createDebug from 'debug';
3
3
  import { withSignal } from '../../../core/with-signal.js';
4
4
  import { AddonAttachmentAmbiguousError, AddonAttachmentNotFoundError, resolveAddonAttachment } from '../../platform/add-on-attachment/resolve.js';
5
- import { getPostgresServiceName, isPostgresAddonInternal } from './postgres-utils.js';
5
+ import { getPostgresServiceName, isPostgresAddon } from './postgres-utils.js';
6
6
  const debug = createDebug('heroku:sdk:resources:database');
7
7
  const DEFAULT_PG_ATTACHMENT = 'DATABASE_URL';
8
8
  /**
@@ -118,7 +118,7 @@ async function listPostgresAttachments(ctx, appIdentity, attachmentId, error, si
118
118
  debug('listing attachments failed while building not-found error: %O', listError);
119
119
  throw new PgDatabaseNotFoundError(`Could not resolve ${stripUrlSuffix(attachmentId)} on ${appIdentity}`, { cause: error });
120
120
  }
121
- return attachments.filter(attachment => isPostgresAddonInternal(attachment.addon));
121
+ return attachments.filter(attachment => isPostgresAddon(attachment.addon));
122
122
  }
123
123
  /**
124
124
  * Used by `attachmentResolutionFallback` to resolve attachments via app config vars
@@ -1,7 +1,63 @@
1
+ import type { AddOnAttachment, AddOnAttachmentCreateOpts } from '@heroku/types/3.sdk';
2
+ import type { PostgresCredentialListResult } from '@heroku/types/data';
1
3
  import type { ResourceCtx } from '../../core/extend-resource.js';
4
+ import type { DeprovisioningAddOn, ResolvedAddOn } from '../platform/add-on/types.js';
2
5
  export type ListCredentialsOptions = {
3
6
  signal?: AbortSignal;
4
7
  };
8
+ export type DeleteAttachmentOptions = {
9
+ beforeDelete?: (preview: {
10
+ addon: ResolvedAddOn;
11
+ attachment: AddOnAttachment;
12
+ }) => Promise<void> | void;
13
+ onDetached?: (attachment: AddOnAttachment) => Promise<void> | void;
14
+ signal?: AbortSignal;
15
+ };
16
+ export type DeleteAttachmentResult = {
17
+ addon: ResolvedAddOn;
18
+ attachment: AddOnAttachment;
19
+ releaseVersion?: number;
20
+ };
21
+ export type PostgresCredential = PostgresCredentialListResult['items'][number];
22
+ export type ListAttachmentsOptions = {
23
+ appIdentity?: string;
24
+ signal?: AbortSignal;
25
+ };
26
+ export type ListAttachmentsResult = {
27
+ addon: ResolvedAddOn;
28
+ attachments: AddOnAttachment[];
29
+ credentials: PostgresCredential[];
30
+ };
31
+ export type CreateAttachmentBody = Omit<AddOnAttachmentCreateOpts, 'addon' | 'app'>;
32
+ export type CreateAttachmentOptions = {
33
+ onAttached?: (attachment: AddOnAttachment) => Promise<void> | void;
34
+ signal?: AbortSignal;
35
+ };
36
+ export type CreateAttachmentResult = {
37
+ addon: ResolvedAddOn;
38
+ attachment: AddOnAttachment;
39
+ releaseVersion?: number;
40
+ };
41
+ export type DestroyOptions = {
42
+ appIdentity?: string;
43
+ /**
44
+ * Runs after the database is resolved but before the delete request fires.
45
+ * An abort (or throw) during the hook prevents the delete — use it to
46
+ * confirm destruction against the resolved add-on.
47
+ */
48
+ beforeDestroy?: (preview: {
49
+ addon: ResolvedAddOn;
50
+ }) => Promise<void> | void;
51
+ force?: boolean;
52
+ onDeprovisioning?: (addon: DeprovisioningAddOn) => Promise<void> | void;
53
+ signal?: AbortSignal;
54
+ wait?: boolean;
55
+ waitIntervalMs?: number;
56
+ };
57
+ export type DestroyResult = {
58
+ addon: ResolvedAddOn;
59
+ result: DeprovisioningAddOn;
60
+ };
5
61
  export type CredentialInfo = {
6
62
  credentials: Array<{
7
63
  connections?: null | number;
@@ -17,8 +73,58 @@ export type CredentialInfo = {
17
73
  uuid: string;
18
74
  };
19
75
  export declare function listCredentials(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: ListCredentialsOptions): Promise<CredentialInfo[]>;
76
+ /**
77
+ * Looks up a Postgres attachment by name under an app, resolves its add-on,
78
+ * and deletes the attachment.
79
+ *
80
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved add-on) if the
81
+ * attachment's database isn't an Advanced-tier database.
82
+ *
83
+ * If provided, `beforeDelete` runs after resolution/tier-checking but before
84
+ * the delete request fires (an abort during the hook prevents the delete), and
85
+ * `onDetached` runs once the attachment is deleted but before the release
86
+ * version fetch. The returned `releaseVersion` is the latest release created by
87
+ * unsetting the attachment's config vars.
88
+ */
89
+ export declare function deleteAttachment(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, attachmentName: string, options?: DeleteAttachmentOptions): Promise<DeleteAttachmentResult>;
90
+ /**
91
+ * Resolves a Postgres database by add-on identity and lists its attachments
92
+ * alongside its credentials, fanning the two requests out in parallel.
93
+ *
94
+ * Resolution is scoped by `appIdentity` when given. Throws
95
+ * `NotAdvancedDatabaseError` (carrying the resolved add-on) if the database
96
+ * isn't an Advanced-tier database.
97
+ */
98
+ export declare function listAttachments(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addonIdentity: string, options?: ListAttachmentsOptions): Promise<ListAttachmentsResult>;
99
+ /**
100
+ * Resolves a source Postgres database by add-on identity, attaches it
101
+ * to an app, and fetches the latest release version.
102
+ *
103
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved add-on) if the
104
+ * database isn't an Advanced-tier database.
105
+ *
106
+ * If provided, `onAttached` fires once the attachment is created but before
107
+ * the release version fetch.
108
+ */
109
+ export declare function createAttachment(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, addonIdentity: string, body?: CreateAttachmentBody, options?: CreateAttachmentOptions): Promise<CreateAttachmentResult>;
110
+ /**
111
+ * Resolves a Postgres database by add-on identity and destroys it.
112
+ *
113
+ * Resolution is scoped by `appIdentity` when given. If provided,
114
+ * `beforeDestroy` runs after resolution but before the delete request fires,
115
+ * so callers can confirm against the resolved add-on; an abort or throw in
116
+ * the hook prevents the delete. Delegates the delete (and optional
117
+ * deprovisioning poll when `wait` is set) to `addOn.destroyAndWait`.
118
+ */
119
+ export declare function destroy(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, options?: DestroyOptions): Promise<DestroyResult>;
20
120
  export declare const postgresDatabaseExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "postgresDatabase", {
121
+ createAttachment: (appIdentity: string, addonIdentity: string, body?: CreateAttachmentBody, options?: CreateAttachmentOptions) => Promise<CreateAttachmentResult>;
122
+ deleteAttachment: (appIdentity: string, attachmentName: string, options?: DeleteAttachmentOptions) => Promise<DeleteAttachmentResult>;
123
+ destroy: (addonIdentity: string, options?: DestroyOptions) => Promise<DestroyResult>;
124
+ listAttachments: (addonIdentity: string, options?: ListAttachmentsOptions) => Promise<ListAttachmentsResult>;
21
125
  listCredentials: (appIdentity: string, addonIdentity?: string, options?: ListCredentialsOptions) => Promise<CredentialInfo[]>;
22
126
  }>;
127
+ export { AddonAppMismatchError, AddonConfirmationRequiredError, AddonDestroyFailedError, AddonNotFoundError, } from '../platform/add-on/errors.js';
128
+ export type { DeprovisioningAddOn, ResolvedAddOn } from '../platform/add-on/types.js';
23
129
  export { type AttachmentFactors, parseAttachmentFactors, } from './internal/parse-attachment-factors.js';
24
- export { findLegacyDatabase, getPostgresServiceName, isAdvancedCredential, isAdvancedDatabase, isEssentialDatabase, isEssentialTier, isLegacyDatabase, isLegacyEssentialDatabase, isPostgresAddon, } from './internal/postgres-utils.js';
130
+ export { findLegacyDatabase, getPostgresServiceName, isAdvancedCredential, isAdvancedDatabase, isEssentialDatabase, isEssentialTier, isLegacyDatabase, isLegacyEssentialDatabase, isPostgresAddon, NotAdvancedDatabaseError, } from './internal/postgres-utils.js';
@@ -1,5 +1,10 @@
1
+ import { HerokuApiError, NotFoundError } from '@heroku/heroku-fetch';
1
2
  import { extendResource } from '../../core/extend-resource.js';
2
3
  import { withSignal } from '../../core/with-signal.js';
4
+ import { destroyAndWait } from '../platform/add-on/destroy-and-wait.js';
5
+ import { AddonAppMismatchError, AddonConfirmationRequiredError, AddonDestroyFailedError, AddonNotFoundError, } from '../platform/add-on/errors.js';
6
+ import { resolveAddonInternal } from '../platform/add-on/resolve.js';
7
+ import { getPostgresServiceName, isAdvancedDatabase, NotAdvancedDatabaseError } from './internal/postgres-utils.js';
3
8
  import { resolvePgDatabase } from './internal/resolve-pg-database.js';
4
9
  export async function listCredentials(ctx, appIdentity, addonIdentity, options = {}) {
5
10
  options.signal?.throwIfAborted();
@@ -8,8 +13,209 @@ export async function listCredentials(ctx, appIdentity, addonIdentity, options =
8
13
  const data = withSignal(ctx.data, options.signal);
9
14
  return data.postgresDatabase.listCredentials(addon.id);
10
15
  }
16
+ /**
17
+ * Looks up a Postgres attachment by name under an app, resolves its add-on,
18
+ * and deletes the attachment.
19
+ *
20
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved add-on) if the
21
+ * attachment's database isn't an Advanced-tier database.
22
+ *
23
+ * If provided, `beforeDelete` runs after resolution/tier-checking but before
24
+ * the delete request fires (an abort during the hook prevents the delete), and
25
+ * `onDetached` runs once the attachment is deleted but before the release
26
+ * version fetch. The returned `releaseVersion` is the latest release created by
27
+ * unsetting the attachment's config vars.
28
+ */
29
+ export async function deleteAttachment(ctx, appIdentity, attachmentName, options = {}) {
30
+ const { beforeDelete, onDetached, signal } = options;
31
+ signal?.throwIfAborted();
32
+ // Bind the signal so the lookup, delete, and release-list requests abort in flight.
33
+ const platform = withSignal(ctx.platform, signal);
34
+ const attachment = await platform.addOnAttachment.infoByApp(appIdentity, attachmentName);
35
+ const addon = await resolveAddonInternal(platform, attachment.addon.name, { addonService: getPostgresServiceName() });
36
+ if (!isAdvancedDatabase(addon)) {
37
+ throw new NotAdvancedDatabaseError(addon, `You can only detach Advanced-tier databases with postgresDatabase.deleteAttachment(). ${addon.name} isn't an Advanced-tier database.`);
38
+ }
39
+ await beforeDelete?.({ addon, attachment });
40
+ signal?.throwIfAborted();
41
+ const deletedAttachment = await platform.addOnAttachment.delete(attachment.id);
42
+ await onDetached?.(deletedAttachment);
43
+ signal?.throwIfAborted();
44
+ // The detach already succeeded; a failed release lookup shouldn't fail the
45
+ // whole operation. Preserve aborts, otherwise report an unknown version.
46
+ let releaseVersion;
47
+ try {
48
+ const releases = await platform
49
+ .withHeaders({ Range: 'version ..; max=1, order=desc' })
50
+ .release.list(appIdentity);
51
+ releaseVersion = releases[0]?.version;
52
+ }
53
+ catch (error) {
54
+ // Preserve aborts; otherwise leave releaseVersion undefined (best-effort).
55
+ if (signal?.aborted) {
56
+ throw error;
57
+ }
58
+ }
59
+ return { addon, attachment: deletedAttachment, releaseVersion };
60
+ }
61
+ /**
62
+ * Resolves a Postgres database by add-on identity and lists its attachments
63
+ * alongside its credentials, fanning the two requests out in parallel.
64
+ *
65
+ * Resolution is scoped by `appIdentity` when given. Throws
66
+ * `NotAdvancedDatabaseError` (carrying the resolved add-on) if the database
67
+ * isn't an Advanced-tier database.
68
+ */
69
+ export async function listAttachments(ctx, addonIdentity, options = {}) {
70
+ const { appIdentity, signal } = options;
71
+ signal?.throwIfAborted();
72
+ // Bind the signal so the resolve and fan-out requests abort in flight.
73
+ const platform = withSignal(ctx.platform, signal);
74
+ const data = withSignal(ctx.data, signal);
75
+ const addon = await resolveAddonInternal(platform, addonIdentity, { addonService: getPostgresServiceName(), appIdentity });
76
+ if (!isAdvancedDatabase(addon)) {
77
+ throw new NotAdvancedDatabaseError(addon, `You can only list attachments on Advanced-tier databases with postgresDatabase.listAttachments(). ${addon.name} isn't an Advanced-tier database.`);
78
+ }
79
+ const [{ items: credentials }, attachments] = await Promise.all([
80
+ data.postgresCredential.list(addon.id),
81
+ platform.addOnAttachment.listByAddOn(addon.id),
82
+ ]);
83
+ return { addon, attachments, credentials };
84
+ }
85
+ /**
86
+ * Resolves a source Postgres database by add-on identity, attaches it
87
+ * to an app, and fetches the latest release version.
88
+ *
89
+ * Throws `NotAdvancedDatabaseError` (carrying the resolved add-on) if the
90
+ * database isn't an Advanced-tier database.
91
+ *
92
+ * If provided, `onAttached` fires once the attachment is created but before
93
+ * the release version fetch.
94
+ */
95
+ export async function createAttachment(ctx, appIdentity, addonIdentity, body = {}, options = {}) {
96
+ const { onAttached, signal } = options;
97
+ signal?.throwIfAborted();
98
+ // Bind the signal so the resolve, create, and release-list requests abort in flight.
99
+ const platform = withSignal(ctx.platform, signal);
100
+ // Resolve scoped to the target app first. If the target app has no match, retry
101
+ // unscoped so attaching a database owned by a *different* app still resolves by
102
+ // its globally-unique name.
103
+ let addon;
104
+ try {
105
+ addon = await resolveAddonInternal(platform, addonIdentity, { addonService: getPostgresServiceName(), appIdentity });
106
+ }
107
+ catch (error) {
108
+ if (!(error instanceof NotFoundError)) {
109
+ throw error;
110
+ }
111
+ addon = await resolveAddonInternal(platform, addonIdentity, { addonService: getPostgresServiceName() });
112
+ }
113
+ if (!isAdvancedDatabase(addon)) {
114
+ throw new NotAdvancedDatabaseError(addon, `You can only attach Advanced-tier databases with postgresDatabase.createAttachment(). ${addon.name} isn't an Advanced-tier database.`);
115
+ }
116
+ let attachment;
117
+ try {
118
+ attachment = await platform.addOnAttachment.create({
119
+ ...body,
120
+ addon: addon.name,
121
+ app: appIdentity,
122
+ });
123
+ }
124
+ catch (error) {
125
+ if (error instanceof HerokuApiError && error.id === 'confirmation_required') {
126
+ throw new AddonConfirmationRequiredError(error.message);
127
+ }
128
+ throw error;
129
+ }
130
+ await onAttached?.(attachment);
131
+ signal?.throwIfAborted();
132
+ const releases = await platform
133
+ .withHeaders({ Range: 'version ..; max=1, order=desc' })
134
+ .release.list(appIdentity);
135
+ return { addon, attachment, releaseVersion: releases[0]?.version };
136
+ }
137
+ /**
138
+ * Resolves a Postgres database by add-on identity and destroys it.
139
+ *
140
+ * Resolution is scoped by `appIdentity` when given. If provided,
141
+ * `beforeDestroy` runs after resolution but before the delete request fires,
142
+ * so callers can confirm against the resolved add-on; an abort or throw in
143
+ * the hook prevents the delete. Delegates the delete (and optional
144
+ * deprovisioning poll when `wait` is set) to `addOn.destroyAndWait`.
145
+ */
146
+ export async function destroy(ctx, addonIdentity, options = {}) {
147
+ const { appIdentity, beforeDestroy, force, onDeprovisioning, signal, wait, waitIntervalMs, } = options;
148
+ signal?.throwIfAborted();
149
+ // Bind the signal so the resolve request aborts in flight.
150
+ const platform = withSignal(ctx.platform, signal);
151
+ let addon;
152
+ try {
153
+ addon = await resolveAddonInternal(platform, addonIdentity, {
154
+ addonService: getPostgresServiceName(), appIdentity,
155
+ });
156
+ }
157
+ catch (error) {
158
+ // When scoped resolution misses, check whether the database exists under a
159
+ // different app so we can surface a targeted mismatch error instead of a
160
+ // bare not-found. Skip the probe for `::`-qualified identities: those name
161
+ // their app inline, so `resolveAddonInternal` ignores `appIdentity` and the
162
+ // unscoped retry would just repeat the same failing request.
163
+ if (appIdentity && !addonIdentity.includes('::') && error instanceof AddonNotFoundError) {
164
+ let elsewhere;
165
+ try {
166
+ elsewhere = await resolveAddonInternal(platform, addonIdentity, {
167
+ addonService: getPostgresServiceName(),
168
+ });
169
+ }
170
+ catch (retryError) {
171
+ // Preserve aborts; an ambiguous/other failure here can't produce a
172
+ // clean mismatch message, so fall through to the original not-found.
173
+ if (signal?.aborted) {
174
+ throw retryError;
175
+ }
176
+ }
177
+ // `appIdentity` may be an app name or UUID; compare against both so a
178
+ // UUID-scoped caller isn't always flagged as a mismatch (and doesn't get
179
+ // a raw UUID echoed back in the error message).
180
+ if (elsewhere && elsewhere.app.id !== appIdentity && elsewhere.app.name !== appIdentity) {
181
+ throw new AddonAppMismatchError(elsewhere, appIdentity);
182
+ }
183
+ }
184
+ throw error;
185
+ }
186
+ await beforeDestroy?.({ addon });
187
+ signal?.throwIfAborted();
188
+ // Resolution and beforeDestroy aborts already happened above; a failure here
189
+ // is the delete request (or wait poll) itself. Preserve aborts, but wrap other
190
+ // failures so callers get the resolved add-on (and thus tier) without
191
+ // re-resolving.
192
+ try {
193
+ const result = await destroyAndWait(ctx, addon.app.id, addon.id, {
194
+ force, onDeprovisioning, signal, wait, waitIntervalMs,
195
+ });
196
+ return { addon, result };
197
+ }
198
+ catch (error) {
199
+ if (signal?.aborted) {
200
+ throw error;
201
+ }
202
+ // A platform `confirmation_required` (e.g. destroying a database attached
203
+ // to other apps without `force`) is a distinct, recoverable condition —
204
+ // surface it as the typed error so callers can prompt and retry, matching
205
+ // `createAttachment`, rather than collapsing it into a generic failure.
206
+ if (error instanceof HerokuApiError && error.id === 'confirmation_required') {
207
+ throw new AddonConfirmationRequiredError(error.message);
208
+ }
209
+ throw new AddonDestroyFailedError(addon, error);
210
+ }
211
+ }
11
212
  export const postgresDatabaseExtensions = extendResource('data', 'postgresDatabase', ctx => ({
213
+ createAttachment: (appIdentity, addonIdentity, body, options) => createAttachment(ctx, appIdentity, addonIdentity, body, options),
214
+ deleteAttachment: (appIdentity, attachmentName, options) => deleteAttachment(ctx, appIdentity, attachmentName, options),
215
+ destroy: (addonIdentity, options) => destroy(ctx, addonIdentity, options),
216
+ listAttachments: (addonIdentity, options) => listAttachments(ctx, addonIdentity, options),
12
217
  listCredentials: (appIdentity, addonIdentity, options) => listCredentials(ctx, appIdentity, addonIdentity, options),
13
218
  }));
219
+ export { AddonAppMismatchError, AddonConfirmationRequiredError, AddonDestroyFailedError, AddonNotFoundError, } from '../platform/add-on/errors.js';
14
220
  export { parseAttachmentFactors, } from './internal/parse-attachment-factors.js';
15
- export { findLegacyDatabase, getPostgresServiceName, isAdvancedCredential, isAdvancedDatabase, isEssentialDatabase, isEssentialTier, isLegacyDatabase, isLegacyEssentialDatabase, isPostgresAddon, } from './internal/postgres-utils.js';
221
+ export { findLegacyDatabase, getPostgresServiceName, isAdvancedCredential, isAdvancedDatabase, isEssentialDatabase, isEssentialTier, isLegacyDatabase, isLegacyEssentialDatabase, isPostgresAddon, NotAdvancedDatabaseError, } from './internal/postgres-utils.js';