@heroku/heroku-cli-util 9.0.2 → 9.1.1

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,16 @@
1
+ import type { ExtendedAddonAttachment } from '../types/pg/data-api';
2
+ /**
3
+ * This error is used internally to signal when the `AddonAttachmentResolver` cannot resolve
4
+ * to a single attachment.
5
+ */
6
+ export declare class AmbiguousError extends Error {
7
+ readonly matches: ExtendedAddonAttachment[];
8
+ readonly type: string;
9
+ readonly body: {
10
+ id: string;
11
+ message: string;
12
+ };
13
+ readonly message: string;
14
+ readonly statusCode = 422;
15
+ constructor(matches: ExtendedAddonAttachment[], type: string);
16
+ }
@@ -1,14 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AmbiguousError = void 0;
4
+ /**
5
+ * This error is used internally to signal when the `AddonAttachmentResolver` cannot resolve
6
+ * to a single attachment.
7
+ */
4
8
  class AmbiguousError extends Error {
5
9
  constructor(matches, type) {
6
10
  super();
7
11
  this.matches = matches;
8
12
  this.type = type;
9
- this.body = { id: 'multiple_matches', message: this.message };
10
13
  this.statusCode = 422;
11
14
  this.message = `Ambiguous identifier; multiple matching add-ons found: ${matches.map(match => match.name).join(', ')}.`;
15
+ this.body = { id: 'multiple_matches', message: this.message };
12
16
  }
13
17
  }
14
18
  exports.AmbiguousError = AmbiguousError;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This error is used only when the Platform API add-on attachment resolver resolves to one or more matches, but
3
+ * a namespace (credential name) was provided and none of them had that exact namespace.
4
+ *
5
+ * We would've expected this to use the same error message the resolver returns when it throws a Not Found error:
6
+ * `"Couldn't find that add on attachment."`, because it's attachments and not add-ons that are being resolved.
7
+ *
8
+ * However, that's not the case here and we cannot refactor this to use the expected messaging because the only
9
+ * command checking credentials, `pg:psql`, has a check with a strict match on the error message text to change
10
+ * the error displayed to the user
11
+ * ([see here](https://github.com/heroku/cli/blob/b79f2c93d6f21eafd9d93983bcd377e4bc7f8438/packages/cli/src/commands/pg/psql.ts#L32)).
12
+ */
13
+ export declare class NotFound extends Error {
14
+ readonly body: {
15
+ id: string;
16
+ message: string;
17
+ };
18
+ readonly message = "Couldn't find that addon.";
19
+ readonly statusCode = 404;
20
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NotFound = void 0;
4
+ /**
5
+ * This error is used only when the Platform API add-on attachment resolver resolves to one or more matches, but
6
+ * a namespace (credential name) was provided and none of them had that exact namespace.
7
+ *
8
+ * We would've expected this to use the same error message the resolver returns when it throws a Not Found error:
9
+ * `"Couldn't find that add on attachment."`, because it's attachments and not add-ons that are being resolved.
10
+ *
11
+ * However, that's not the case here and we cannot refactor this to use the expected messaging because the only
12
+ * command checking credentials, `pg:psql`, has a check with a strict match on the error message text to change
13
+ * the error displayed to the user
14
+ * ([see here](https://github.com/heroku/cli/blob/b79f2c93d6f21eafd9d93983bcd377e4bc7f8438/packages/cli/src/commands/pg/psql.ts#L32)).
15
+ */
16
+ class NotFound extends Error {
17
+ constructor() {
18
+ super(...arguments);
19
+ this.body = { id: 'not_found', message: 'Couldn\'t find that addon.' };
20
+ this.message = 'Couldn\'t find that addon.';
21
+ this.statusCode = 404;
22
+ }
23
+ }
24
+ exports.NotFound = NotFound;
package/dist/index.d.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { AmbiguousError } from './types/errors/ambiguous';
2
- import { NotFound } from './types/errors/not-found';
3
- import { AddOnAttachmentWithConfigVarsAndPlan, AddOnWithRelatedData, Link } from './types/pg/data-api';
1
+ import { APIClient } from '@heroku-cli/command';
2
+ import { AmbiguousError } from './errors/ambiguous';
3
+ import { NotFound } from './errors/not-found';
4
+ import { AddOnWithRelatedData, ExtendedAddonAttachment, Link } from './types/pg/data-api';
4
5
  import { ConnectionDetails, ConnectionDetailsWithAttachment, TunnelConfig } from './types/pg/tunnel';
5
- import { getDatabase } from './utils/pg/databases';
6
6
  import getHost from './utils/pg/host';
7
- import { exec } from './utils/pg/psql';
8
7
  import { confirm } from './ux/confirm';
9
8
  import { prompt } from './ux/prompt';
10
9
  import { styledHeader } from './ux/styled-header';
@@ -12,26 +11,31 @@ import { styledJSON } from './ux/styled-json';
12
11
  import { styledObject } from './ux/styled-object';
13
12
  import { table } from './ux/table';
14
13
  import { wait } from './ux/wait';
14
+ export type { AddOnWithRelatedData, ExtendedAddonAttachment, Link, } from './types/pg/data-api';
15
+ export type { ConnectionDetails, ConnectionDetailsWithAttachment, TunnelConfig, } from './types/pg/tunnel';
16
+ /** @deprecated Use direct type imports instead */
15
17
  export declare const types: {
16
- errors: {
17
- AmbiguousError: typeof AmbiguousError;
18
- NotFound: typeof NotFound;
19
- };
20
18
  pg: {
21
- AddOnAttachmentWithConfigVarsAndPlan: AddOnAttachmentWithConfigVarsAndPlan;
22
19
  AddOnWithRelatedData: AddOnWithRelatedData;
23
20
  ConnectionDetails: ConnectionDetails;
24
21
  ConnectionDetailsWithAttachment: ConnectionDetailsWithAttachment;
22
+ ExtendedAddonAttachment: ExtendedAddonAttachment;
25
23
  Link: Link;
26
24
  TunnelConfig: TunnelConfig;
27
25
  };
28
26
  };
29
27
  export declare const utils: {
28
+ errors: {
29
+ AmbiguousError: typeof AmbiguousError;
30
+ NotFound: typeof NotFound;
31
+ };
30
32
  pg: {
31
- databases: typeof getDatabase;
33
+ fetcher: {
34
+ database(heroku: APIClient, appId: string, attachmentId?: string, namespace?: string): Promise<ConnectionDetailsWithAttachment>;
35
+ };
32
36
  host: typeof getHost;
33
37
  psql: {
34
- exec: typeof exec;
38
+ exec(connectionDetails: ConnectionDetailsWithAttachment, query: string, psqlCmdArgs?: string[]): Promise<string>;
35
39
  };
36
40
  };
37
41
  };
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.hux = exports.utils = exports.types = void 0;
4
- const ambiguous_1 = require("./types/errors/ambiguous");
5
- const not_found_1 = require("./types/errors/not-found");
6
- const databases_1 = require("./utils/pg/databases");
7
- const host_1 = require("./utils/pg/host");
8
- const psql_1 = require("./utils/pg/psql");
4
+ const tslib_1 = require("tslib");
5
+ const ambiguous_1 = require("./errors/ambiguous");
6
+ const not_found_1 = require("./errors/not-found");
7
+ const databases_1 = tslib_1.__importDefault(require("./utils/pg/databases"));
8
+ const host_1 = tslib_1.__importDefault(require("./utils/pg/host"));
9
+ const psql_1 = tslib_1.__importDefault(require("./utils/pg/psql"));
9
10
  const confirm_1 = require("./ux/confirm");
10
11
  const prompt_1 = require("./ux/prompt");
11
12
  const styled_header_1 = require("./ux/styled-header");
@@ -13,26 +14,36 @@ const styled_json_1 = require("./ux/styled-json");
13
14
  const styled_object_1 = require("./ux/styled-object");
14
15
  const table_1 = require("./ux/table");
15
16
  const wait_1 = require("./ux/wait");
17
+ // Keep const types for backward compatibility (deprecated)
18
+ /** @deprecated Use direct type imports instead */
16
19
  exports.types = {
17
- errors: {
18
- AmbiguousError: ambiguous_1.AmbiguousError,
19
- NotFound: not_found_1.NotFound,
20
- },
21
20
  pg: {
22
- AddOnAttachmentWithConfigVarsAndPlan: {},
23
21
  AddOnWithRelatedData: {},
24
22
  ConnectionDetails: {},
25
23
  ConnectionDetailsWithAttachment: {},
24
+ ExtendedAddonAttachment: {},
26
25
  Link: {},
27
26
  TunnelConfig: {},
28
27
  },
29
28
  };
30
29
  exports.utils = {
30
+ errors: {
31
+ AmbiguousError: ambiguous_1.AmbiguousError,
32
+ NotFound: not_found_1.NotFound, // This should be NotFoundError for consistency, but we're keeping it for backwards compatibility
33
+ },
31
34
  pg: {
32
- databases: databases_1.getDatabase,
35
+ fetcher: {
36
+ database(heroku, appId, attachmentId, namespace) {
37
+ const databaseResolver = new databases_1.default(heroku);
38
+ return databaseResolver.getDatabase(appId, attachmentId, namespace);
39
+ },
40
+ },
33
41
  host: host_1.default,
34
42
  psql: {
35
- exec: psql_1.exec,
43
+ exec(connectionDetails, query, psqlCmdArgs = []) {
44
+ const psqlService = new psql_1.default(connectionDetails);
45
+ return psqlService.execQuery(query, psqlCmdArgs);
46
+ },
36
47
  },
37
48
  },
38
49
  };
@@ -1,13 +1,41 @@
1
1
  import * as Heroku from '@heroku-cli/schema';
2
+ type DeepRequired<T> = T extends object ? {
3
+ [K in keyof T]-?: DeepRequired<T[K]>;
4
+ } : T;
5
+ /**
6
+ * This is the base type for the property `addon` on an `AddOnAttachment` as described in the Platform API Reference.
7
+ */
8
+ type AddonDescriptor = DeepRequired<Heroku.AddOnAttachment>['addon'];
9
+ /**
10
+ * This is the modified type for the `addon` property when the request to Platform API includes the value `addon:plan`
11
+ * in the `Accept-Inclusion` header.
12
+ */
13
+ type AddonDescriptorWithPlanInclusion = {
14
+ plan: {
15
+ id: string;
16
+ name: string;
17
+ };
18
+ } & AddonDescriptor;
19
+ /**
20
+ * This is the modified type for the `AddOnAttachment` type when the request to Platform API includes the value
21
+ * `config_vars` in the `Accept-Inclusion` header.
22
+ */
23
+ type AddonAttachmentWithConfigVarsInclusion = {
24
+ config_vars: string[];
25
+ } & DeepRequired<Heroku.AddOnAttachment>;
26
+ /**
27
+ * This is the modified type for the `AddOnAttachment` we use on these lib functions because all requests made to
28
+ * Platform API to get add-on attachments, either through the Add-on Attachment List endpoint or the
29
+ * add-on attachment resolver action endpoint, include the header `Accept-Inclusion: addon:plan,config_vars`.
30
+ */
31
+ export type ExtendedAddonAttachment = {
32
+ addon: AddonDescriptorWithPlanInclusion;
33
+ } & AddonAttachmentWithConfigVarsInclusion;
2
34
  export type AddOnWithRelatedData = {
3
35
  attachment_names?: string[];
4
36
  links?: Link[];
5
- plan: Required<Heroku.AddOn['plan']>;
6
- } & Required<Heroku.AddOnAttachment['addon']>;
7
- export type AddOnAttachmentWithConfigVarsAndPlan = {
8
- addon: AddOnWithRelatedData;
9
- config_vars: Heroku.AddOn['config_vars'];
10
- } & Required<Heroku.AddOnAttachment>;
37
+ plan: DeepRequired<Heroku.AddOn['plan']>;
38
+ } & AddonDescriptor;
11
39
  export type Link = {
12
40
  attachment_name?: string;
13
41
  created_at: string;
@@ -15,3 +43,4 @@ export type Link = {
15
43
  name: string;
16
44
  remote?: Link;
17
45
  };
46
+ export {};
@@ -1,11 +1,7 @@
1
- import type { AddOnAttachment } from '@heroku-cli/schema';
2
1
  import { Server } from 'node:net';
3
- import * as createTunnel from 'tunnel-ssh';
4
- import type { AddOnAttachmentWithConfigVarsAndPlan } from './data-api';
2
+ import type { ExtendedAddonAttachment } from './data-api';
5
3
  export type ConnectionDetails = {
6
4
  _tunnel?: Server;
7
- bastionHost?: string;
8
- bastionKey?: string;
9
5
  database: string;
10
6
  host: string;
11
7
  password: string;
@@ -13,10 +9,24 @@ export type ConnectionDetails = {
13
9
  port: string;
14
10
  url: string;
15
11
  user: string;
16
- };
12
+ } & BastionConfig;
17
13
  export type ConnectionDetailsWithAttachment = {
18
- attachment: Required<{
19
- addon: AddOnAttachmentWithConfigVarsAndPlan;
20
- } & AddOnAttachment>;
14
+ attachment: ExtendedAddonAttachment;
21
15
  } & ConnectionDetails;
22
- export type TunnelConfig = createTunnel.Config;
16
+ export interface TunnelConfig {
17
+ dstHost: string;
18
+ dstPort: number;
19
+ host?: string;
20
+ localHost: string;
21
+ localPort: number;
22
+ privateKey?: string;
23
+ username: string;
24
+ }
25
+ export interface BastionConfigResponse {
26
+ host: string;
27
+ private_key: string;
28
+ }
29
+ export type BastionConfig = {
30
+ bastionHost?: string;
31
+ bastionKey?: string;
32
+ };
@@ -1,9 +1,13 @@
1
1
  import type { APIClient } from '@heroku-cli/command';
2
- import type { AddOnAttachment } from '@heroku-cli/schema';
3
- import type { AddOnAttachmentWithConfigVarsAndPlan } from '../../types/pg/data-api';
4
- export declare const appAttachment: (heroku: APIClient, app: string | undefined, id: string, options?: {
5
- addon_service?: string;
2
+ import type { ExtendedAddonAttachment } from '../../types/pg/data-api';
3
+ export interface AddonAttachmentResolverOptions {
4
+ addonService?: string;
6
5
  namespace?: string;
7
- }) => Promise<{
8
- addon: AddOnAttachmentWithConfigVarsAndPlan;
9
- } & AddOnAttachment>;
6
+ }
7
+ export default class AddonAttachmentResolver {
8
+ private readonly heroku;
9
+ private readonly attachmentHeaders;
10
+ constructor(heroku: APIClient);
11
+ resolve(appId: string | undefined, attachmentId: string, options?: AddonAttachmentResolverOptions): Promise<ExtendedAddonAttachment>;
12
+ private singularize;
13
+ }
@@ -1,28 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.appAttachment = void 0;
4
- const ambiguous_1 = require("../../types/errors/ambiguous");
5
- const not_found_1 = require("../../types/errors/not-found");
6
- const appAttachment = async (heroku, app, id, options = {}) => {
7
- const result = await heroku.post('/actions/addon-attachments/resolve', {
8
- // eslint-disable-next-line camelcase
9
- body: { addon_attachment: id, addon_service: options.addon_service, app }, headers: attachmentHeaders,
10
- });
11
- return singularize('addon_attachment', options.namespace)(result.body);
12
- };
13
- exports.appAttachment = appAttachment;
14
- const attachmentHeaders = {
15
- Accept: 'application/vnd.heroku+json; version=3.sdk',
16
- 'Accept-Inclusion': 'addon:plan,config_vars',
17
- };
18
- function singularize(type, namespace) {
19
- return (matches) => {
3
+ const ambiguous_1 = require("../../errors/ambiguous");
4
+ const not_found_1 = require("../../errors/not-found");
5
+ class AddonAttachmentResolver {
6
+ constructor(heroku) {
7
+ this.heroku = heroku;
8
+ this.attachmentHeaders = {
9
+ Accept: 'application/vnd.heroku+json; version=3.sdk',
10
+ 'Accept-Inclusion': 'addon:plan,config_vars',
11
+ };
12
+ }
13
+ async resolve(appId, attachmentId, options = {}) {
14
+ const { body: attachments } = await this.heroku.post('/actions/addon-attachments/resolve', {
15
+ // eslint-disable-next-line camelcase
16
+ body: { addon_attachment: attachmentId, addon_service: options.addonService, app: appId },
17
+ headers: this.attachmentHeaders,
18
+ });
19
+ return this.singularize(attachments, options.namespace);
20
+ }
21
+ singularize(attachments, namespace) {
22
+ let matches;
20
23
  if (namespace) {
21
- matches = matches.filter(m => m.namespace === namespace);
24
+ matches = attachments.filter(m => m.namespace === namespace);
22
25
  }
23
- else if (matches.length > 1) {
24
- // In cases that aren't specific enough, filter by namespace
25
- matches = matches.filter(m => !Reflect.has(m, 'namespace') || m.namespace === null);
26
+ else if (attachments.length > 1) {
27
+ // In cases that aren't specific enough, keep only attachments without a namespace
28
+ matches = attachments.filter(m => !Reflect.has(m, 'namespace') || m.namespace === null);
29
+ }
30
+ else {
31
+ matches = attachments;
26
32
  }
27
33
  switch (matches.length) {
28
34
  case 0: {
@@ -32,8 +38,9 @@ function singularize(type, namespace) {
32
38
  return matches[0];
33
39
  }
34
40
  default: {
35
- throw new ambiguous_1.AmbiguousError(matches, type !== null && type !== void 0 ? type : '');
41
+ throw new ambiguous_1.AmbiguousError(matches, 'addon_attachment');
36
42
  }
37
43
  }
38
- };
44
+ }
39
45
  }
46
+ exports.default = AddonAttachmentResolver;
@@ -1,30 +1,71 @@
1
1
  import type { APIClient } from '@heroku-cli/command';
2
- import * as createTunnel from 'tunnel-ssh';
3
- import { AddOnAttachmentWithConfigVarsAndPlan } from '../../types/pg/data-api';
4
- import { ConnectionDetails } from '../../types/pg/tunnel';
5
- import { TunnelConfig } from '../../types/pg/tunnel';
6
- export declare const bastionKeyPlan: (a: AddOnAttachmentWithConfigVarsAndPlan) => boolean;
7
- export declare const env: (db: ConnectionDetails) => {
8
- TZ?: string;
9
- PGAPPNAME: string;
10
- PGSSLMODE: string;
11
- };
12
- export declare function fetchConfig(heroku: APIClient, db: {
13
- id: string;
14
- }): Promise<import("@heroku/http-call").HTTP<{
15
- host: string;
16
- private_key: string;
17
- }>>;
18
- export declare const getBastion: (config: Record<string, string>, baseName: string) => {
19
- bastionHost: string;
20
- bastionKey: string;
21
- } | {
22
- bastionHost?: undefined;
23
- bastionKey?: undefined;
24
- };
25
- export declare function getConfigs(db: ConnectionDetails): {
2
+ import { Server } from 'node:net';
3
+ import { ExtendedAddonAttachment } from '../../types/pg/data-api';
4
+ import { BastionConfig, ConnectionDetailsWithAttachment, TunnelConfig } from '../../types/pg/tunnel';
5
+ /**
6
+ * Determines whether the attachment belongs to an add-on installed onto a non-shield Private Space.
7
+ * If true, the bastion information needs to be fetched from the Data API.
8
+ * For add-ons installed onto a Shield Private Space, the bastion information should be fetched from config vars.
9
+ *
10
+ * @param attachment - The add-on attachment to check
11
+ * @returns True if the attachment belongs to a non-shield Private Space, false otherwise
12
+ */
13
+ export declare function bastionKeyPlan(attachment: ExtendedAddonAttachment): boolean;
14
+ /**
15
+ * Fetches the bastion configuration from the Data API (only relevant for add-ons installed onto a
16
+ * non-shield Private Space).
17
+ * For add-ons installed onto a Shield Private Space, the bastion information is stored in the config vars.
18
+ *
19
+ * @param heroku - The Heroku API client
20
+ * @param addon - The add-on information
21
+ * @returns Promise that resolves to the bastion configuration
22
+ */
23
+ export declare function fetchBastionConfig(heroku: APIClient, addon: ExtendedAddonAttachment['addon']): Promise<BastionConfig>;
24
+ /**
25
+ * Returns the bastion configuration from the config vars for add-ons installed onto Shield
26
+ * Private Spaces.
27
+ *
28
+ * If there are bastions, extracts a host and a key from the config vars.
29
+ * If there are no bastions, returns an empty Object.
30
+ *
31
+ * We assert that _BASTIONS and _BASTION_KEY always exist together.
32
+ * If either is falsy, pretend neither exist.
33
+ *
34
+ * @param config - The configuration variables object
35
+ * @param baseName - The base name for the configuration variables
36
+ * @returns The bastion configuration object
37
+ */
38
+ export declare const getBastionConfig: (config: Record<string, string>, baseName: string) => BastionConfig;
39
+ /**
40
+ * Returns both the required environment variables to effect the psql command execution and the tunnel
41
+ * configuration according to the database connection details.
42
+ *
43
+ * @param connectionDetails - The database connection details with attachment information
44
+ * @returns Object containing database environment variables and tunnel configuration
45
+ */
46
+ export declare function getPsqlConfigs(connectionDetails: ConnectionDetailsWithAttachment): {
26
47
  dbEnv: NodeJS.ProcessEnv;
27
- dbTunnelConfig: createTunnel.Config;
48
+ dbTunnelConfig: TunnelConfig;
28
49
  };
29
- export declare function sshTunnel(db: ConnectionDetails, dbTunnelConfig: TunnelConfig, timeout?: number): Promise<void | import("net").Server | null>;
30
- export declare function tunnelConfig(db: ConnectionDetails): TunnelConfig;
50
+ export type PsqlConfigs = ReturnType<typeof getPsqlConfigs>;
51
+ /**
52
+ * Establishes an SSH tunnel to the database using the provided configuration.
53
+ *
54
+ * @param connectionDetails - The database connection details with attachment information
55
+ * @param dbTunnelConfig - The tunnel configuration object
56
+ * @param timeout - The timeout in milliseconds (default: 10000)
57
+ * @param createSSHTunnel - The function to create the SSH tunnel
58
+ * @returns Promise that resolves to the tunnel server or null if no bastion key is provided
59
+ * @throws Error if unable to establish the tunnel
60
+ */
61
+ export declare function sshTunnel(connectionDetails: ConnectionDetailsWithAttachment, dbTunnelConfig: TunnelConfig, timeout?: number, createSSHTunnel?: typeof createSSHTunnelAdapter): Promise<Server | void>;
62
+ /**
63
+ * Adapter for tunnel-ssh v5 API. Translates our TunnelConfig into the v5
64
+ * createTunnel(tunnelOptions, serverOptions, sshOptions, forwardOptions) call
65
+ * and returns the created local Server.
66
+ *
67
+ * @param config - The tunnel configuration to translate for v5 API
68
+ * @returns Promise that resolves to the created local TCP Server
69
+ */
70
+ declare function createSSHTunnelAdapter(config: TunnelConfig): Promise<Server>;
71
+ export {};