@heroku/sdk 0.6.0 → 0.6.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.
package/README.md CHANGED
@@ -28,12 +28,42 @@ const favorites = await sdk.dashboardBackend.favorite.list({ type: 'app' })
28
28
  await sdk.platform.app.enableMaintenance('my-app')
29
29
  ```
30
30
 
31
+ Extensions can also coordinate multiple services. For example, resolve a
32
+ pipeline first, then resolve its GitHub repository through Repositories API
33
+ with the Kolkrabbi-backed repositories service as a fallback:
34
+
35
+ ```ts
36
+ import { HerokuSDK } from '@heroku/sdk'
37
+ import { reviewAppConfigExtensions } from '@heroku/sdk/extensions/platform'
38
+
39
+ const sdk = new HerokuSDK({ extensions: [reviewAppConfigExtensions] })
40
+ const pipeline = await sdk.platform.pipeline.info('my-pipeline')
41
+
42
+ if (!pipeline.id) throw new Error('Pipeline response did not include an id')
43
+ const repo = await sdk.platform.reviewAppConfig.resolveRepoName(pipeline.id)
44
+ ```
45
+
31
46
  You can also pass options directly:
32
47
 
33
48
  ```ts
34
49
  const sdk = new HerokuSDK({ clientOptions: { token: 'your-api-token' } })
35
50
  ```
36
51
 
52
+ Common client options apply to every service. In particular, a common `baseUrl`
53
+ overrides every service's default host, including Metrics and Repositories. Use
54
+ shallow per-service overrides when only specific services should use a host or
55
+ other client setting:
56
+
57
+ ```ts
58
+ const sdk = new HerokuSDK({
59
+ clientOptions: { token: 'your-api-token' },
60
+ clientOptionsByService: {
61
+ platform: { baseUrl: 'https://api.heroku.com' },
62
+ repositoriesApi: { baseUrl: 'https://api.heroku.com' },
63
+ },
64
+ })
65
+ ```
66
+
37
67
  ### Per-service clients
38
68
 
39
69
  If you only need one service (and want the smallest possible bundle), import the factory directly. These return the same typed client the SDK class wraps internally.
@@ -44,12 +74,14 @@ import { createDataClient } from '@heroku/sdk/data'
44
74
  import { createDashboardBackendClient } from '@heroku/sdk/dashboard-backend'
45
75
  import { createMetricsClient } from '@heroku/sdk/metrics'
46
76
  import { createRepositoriesClient } from '@heroku/sdk/repositories'
77
+ import { createRepositoriesApiClient } from '@heroku/sdk/repositories-api'
47
78
 
48
79
  const platform = createPlatformClient()
49
80
  const data = createDataClient()
50
81
  const dashboardBackend = createDashboardBackendClient()
51
82
  const metrics = createMetricsClient()
52
83
  const repositories = createRepositoriesClient()
84
+ const repositoriesApi = createRepositoriesApiClient()
53
85
 
54
86
  const apps = await platform.app.list()
55
87
  const favorites = await dashboardBackend.favorite.list({ type: 'app' })
@@ -65,6 +97,7 @@ const favorites = await dashboardBackend.favorite.list({ type: 'app' })
65
97
  | `createDashboardBackendClient`, `DashboardBackendClient` | `@heroku/sdk/dashboard-backend` |
66
98
  | `createMetricsClient`, `MetricsClient` | `@heroku/sdk/metrics` |
67
99
  | `createRepositoriesClient`, `RepositoriesClient` | `@heroku/sdk/repositories` |
100
+ | `createRepositoriesApiClient`, `RepositoriesApiClient` | `@heroku/sdk/repositories-api` |
68
101
  | `appExtensions`, `dynoExtensions`, … | `@heroku/sdk/extensions/platform` |
69
102
  | `databaseExtensions`, … | `@heroku/sdk/extensions/data` |
70
103
 
@@ -2,15 +2,20 @@ import type { DashboardBackendClient } from '../services/dashboard-backend.js';
2
2
  import type { DataClient } from '../services/data.js';
3
3
  import type { MetricsClient } from '../services/metrics.js';
4
4
  import type { PlatformClient } from '../services/platform.js';
5
+ import type { RepositoriesApiClient } from '../services/repositories-api.js';
5
6
  import type { RepositoriesClient } from '../services/repositories.js';
6
- export type ServiceName = 'dashboardBackend' | 'data' | 'metrics' | 'platform' | 'repositories';
7
- export type ResourceCtx = {
7
+ export type ServiceName = 'dashboardBackend' | 'data' | 'metrics' | 'platform' | 'repositories' | 'repositoriesApi';
8
+ type BaseResourceCtx = {
8
9
  dashboardBackend: DashboardBackendClient;
9
10
  data: DataClient;
10
11
  metrics: MetricsClient;
11
12
  platform: PlatformClient;
12
13
  repositories: RepositoriesClient;
13
14
  };
15
+ export type ResourceCtx = BaseResourceCtx & {
16
+ /** Available to SDK-provided extensions; optional for compatibility with existing extension contexts. */
17
+ repositoriesApi?: RepositoriesApiClient;
18
+ };
14
19
  export type ResourceMethods = Record<string, (...args: any[]) => any>;
15
20
  export type ResourceExtension<S extends ServiceName = ServiceName, R extends string = string, M extends ResourceMethods = ResourceMethods> = {
16
21
  factory: (ctx: ResourceCtx) => M;
@@ -3,10 +3,16 @@ import type { DashboardBackendClient } from '../services/dashboard-backend.js';
3
3
  import type { DataClient } from '../services/data.js';
4
4
  import type { MetricsClient } from '../services/metrics.js';
5
5
  import type { PlatformClient } from '../services/platform.js';
6
+ import type { RepositoriesApiClient } from '../services/repositories-api.js';
6
7
  import type { RepositoriesClient } from '../services/repositories.js';
7
- import type { ApplyExtensions, ExtensionsFor, ResourceExtension } from './extend-resource.js';
8
+ import type { ApplyExtensions, ExtensionsFor, ResourceExtension, ServiceName } from './extend-resource.js';
9
+ type ServiceClientOptions = Omit<HerokuApiClientOptions, 'service'> & {
10
+ service?: never;
11
+ };
8
12
  export type HerokuSDKOptions<Exts extends readonly ResourceExtension[]> = {
9
13
  clientOptions?: HerokuApiClientOptions;
14
+ /** Shallow per-service overrides applied after common clientOptions. */
15
+ clientOptionsByService?: Partial<Record<ServiceName, ServiceClientOptions>>;
10
16
  extensions?: Exts;
11
17
  };
12
18
  export declare class HerokuSDK<const Exts extends readonly ResourceExtension[] = readonly ResourceExtension[]> {
@@ -17,4 +23,6 @@ export declare class HerokuSDK<const Exts extends readonly ResourceExtension[] =
17
23
  get metrics(): ApplyExtensions<MetricsClient, ExtensionsFor<Exts, 'metrics'>>;
18
24
  get platform(): ApplyExtensions<PlatformClient, ExtensionsFor<Exts, 'platform'>>;
19
25
  get repositories(): ApplyExtensions<RepositoriesClient, ExtensionsFor<Exts, 'repositories'>>;
26
+ get repositoriesApi(): ApplyExtensions<RepositoriesApiClient, ExtensionsFor<Exts, 'repositoriesApi'>>;
20
27
  }
28
+ export {};
@@ -2,6 +2,7 @@ import { createDashboardBackendClient } from '../services/dashboard-backend.js';
2
2
  import { createDataClient } from '../services/data.js';
3
3
  import { createMetricsClient } from '../services/metrics.js';
4
4
  import { createPlatformClient } from '../services/platform.js';
5
+ import { createRepositoriesApiClient } from '../services/repositories-api.js';
5
6
  import { createRepositoriesClient } from '../services/repositories.js';
6
7
  import { mergeExtensions } from './extensions-proxy.js';
7
8
  function partitionByService(extensions) {
@@ -15,6 +16,7 @@ function partitionByService(extensions) {
15
16
  }
16
17
  export class HerokuSDK {
17
18
  #clientOptions;
19
+ #clientOptionsByService;
18
20
  #ctx;
19
21
  #dashboardBackend;
20
22
  #data;
@@ -26,9 +28,12 @@ export class HerokuSDK {
26
28
  #rawMetrics;
27
29
  #rawPlatform;
28
30
  #rawRepositories;
31
+ #rawRepositoriesApi;
29
32
  #repositories;
33
+ #repositoriesApi;
30
34
  constructor(options = {}) {
31
35
  this.#clientOptions = options.clientOptions ?? {};
36
+ this.#clientOptionsByService = options.clientOptionsByService ?? {};
32
37
  this.#extensionsByService = partitionByService(options.extensions ?? []);
33
38
  }
34
39
  get dashboardBackend() {
@@ -51,6 +56,19 @@ export class HerokuSDK {
51
56
  this.#repositories ??= mergeExtensions(this.#getRawRepositories(), this.#extensionsByService.get('repositories') ?? [], this.#getCtx());
52
57
  return this.#repositories;
53
58
  }
59
+ get repositoriesApi() {
60
+ this.#repositoriesApi ??= mergeExtensions(this.#getRawRepositoriesApi(), this.#extensionsByService.get('repositoriesApi') ?? [], this.#getCtx());
61
+ return this.#repositoriesApi;
62
+ }
63
+ #getClientOptions(service) {
64
+ const serviceOverride = this.#clientOptionsByService[service];
65
+ const serviceOptions = { ...serviceOverride };
66
+ delete serviceOptions.service;
67
+ return {
68
+ ...this.#clientOptions,
69
+ ...serviceOptions,
70
+ };
71
+ }
54
72
  #getCtx() {
55
73
  this.#ctx ??= Object.defineProperties({}, {
56
74
  dashboardBackend: {
@@ -73,27 +91,35 @@ export class HerokuSDK {
73
91
  enumerable: true,
74
92
  get: () => this.#getRawRepositories(),
75
93
  },
94
+ repositoriesApi: {
95
+ enumerable: true,
96
+ get: () => this.#getRawRepositoriesApi(),
97
+ },
76
98
  });
77
99
  return this.#ctx;
78
100
  }
79
101
  #getRawDashboardBackend() {
80
- this.#rawDashboardBackend ??= createDashboardBackendClient(this.#clientOptions);
102
+ this.#rawDashboardBackend ??= createDashboardBackendClient(this.#getClientOptions('dashboardBackend'));
81
103
  return this.#rawDashboardBackend;
82
104
  }
83
105
  #getRawData() {
84
- this.#rawData ??= createDataClient(this.#clientOptions);
106
+ this.#rawData ??= createDataClient(this.#getClientOptions('data'));
85
107
  return this.#rawData;
86
108
  }
87
109
  #getRawMetrics() {
88
- this.#rawMetrics ??= createMetricsClient(this.#clientOptions);
110
+ this.#rawMetrics ??= createMetricsClient(this.#getClientOptions('metrics'));
89
111
  return this.#rawMetrics;
90
112
  }
91
113
  #getRawPlatform() {
92
- this.#rawPlatform ??= createPlatformClient(this.#clientOptions);
114
+ this.#rawPlatform ??= createPlatformClient(this.#getClientOptions('platform'));
93
115
  return this.#rawPlatform;
94
116
  }
95
117
  #getRawRepositories() {
96
- this.#rawRepositories ??= createRepositoriesClient(this.#clientOptions);
118
+ this.#rawRepositories ??= createRepositoriesClient(this.#getClientOptions('repositories'));
97
119
  return this.#rawRepositories;
98
120
  }
121
+ #getRawRepositoriesApi() {
122
+ this.#rawRepositoriesApi ??= createRepositoriesApiClient(this.#getClientOptions('repositoriesApi'));
123
+ return this.#rawRepositoriesApi;
124
+ }
99
125
  }
@@ -7,6 +7,7 @@ export { type AttachmentAttachedEvent, type AttachmentDetachedEvent, type Attach
7
7
  export { pipelineCouplingExtensions } from '../platform/pipeline/coupling.js';
8
8
  export { pipelineExtensions } from '../platform/pipeline/index.js';
9
9
  export { pipelinePromotionExtensions } from '../platform/pipeline/promotion.js';
10
+ export { type ResolveRepoNameOptions, reviewAppConfigExtensions } from '../platform/review-app-config/index.js';
10
11
  export { privateToShield, shieldToPrivate } from '../platform/shield.js';
11
12
  export { createAndAssociate, type CreateAndAssociateOptions, sniEndpointExtensions } from '../platform/sni-endpoint/index.js';
12
13
  export { spaceExtensions, SpaceNotAllocatedError, SpaceNotReadyError, type SpaceOptions, type SpaceWithNat, waitForAllocated, type WaitForAllocatedOptions, } from '../platform/space/index.js';
@@ -7,6 +7,7 @@ export { logSessionExtensions, parseHerokuLogLine, } from '../platform/log-sessi
7
7
  export { pipelineCouplingExtensions } from '../platform/pipeline/coupling.js';
8
8
  export { pipelineExtensions } from '../platform/pipeline/index.js';
9
9
  export { pipelinePromotionExtensions } from '../platform/pipeline/promotion.js';
10
+ export { reviewAppConfigExtensions } from '../platform/review-app-config/index.js';
10
11
  export { privateToShield, shieldToPrivate } from '../platform/shield.js';
11
12
  export { createAndAssociate, sniEndpointExtensions } from '../platform/sni-endpoint/index.js';
12
13
  export { spaceExtensions, SpaceNotAllocatedError, SpaceNotReadyError, waitForAllocated, } from '../platform/space/index.js';
@@ -0,0 +1,2 @@
1
+ import createDebug from 'debug';
2
+ export declare const debug: createDebug.Debugger;
@@ -0,0 +1,2 @@
1
+ import createDebug from 'debug';
2
+ export const debug = createDebug('heroku:sdk:resources:review-app-config');
@@ -0,0 +1,5 @@
1
+ import { type ResolveRepoNameOptions } from './resolve-repo-name.js';
2
+ export { resolveRepoName, type ResolveRepoNameOptions } from './resolve-repo-name.js';
3
+ export declare const reviewAppConfigExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "reviewAppConfig", {
4
+ resolveRepoName: (pipelineId: string, options?: ResolveRepoNameOptions) => Promise<string>;
5
+ }>;
@@ -0,0 +1,17 @@
1
+ import { extendResource } from '../../../core/extend-resource.js';
2
+ import { resolveRepoName } from './resolve-repo-name.js';
3
+ export { resolveRepoName } from './resolve-repo-name.js';
4
+ export const reviewAppConfigExtensions = extendResource('platform', 'reviewAppConfig', ctx => {
5
+ const { repositoriesApi } = ctx;
6
+ if (!repositoriesApi) {
7
+ throw new Error('reviewAppConfigExtensions requires ResourceCtx.repositoriesApi');
8
+ }
9
+ const resolveRepoNameCtx = {
10
+ platform: ctx.platform,
11
+ repositories: ctx.repositories,
12
+ repositoriesApi,
13
+ };
14
+ return {
15
+ resolveRepoName: (pipelineId, options) => resolveRepoName(resolveRepoNameCtx, pipelineId, options),
16
+ };
17
+ });
@@ -0,0 +1,16 @@
1
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
2
+ type ResolveRepoNameCtx = Pick<ResourceCtx, 'platform' | 'repositories'> & {
3
+ repositoriesApi: NonNullable<ResourceCtx['repositoriesApi']>;
4
+ };
5
+ export type ResolveRepoNameOptions = {
6
+ signal?: AbortSignal;
7
+ };
8
+ /**
9
+ * Resolves the GitHub repository name for a canonical pipeline ID.
10
+ * Resolve user-supplied pipeline names before calling this helper.
11
+ * Uses Repositories API when the account feature is enabled, and falls back
12
+ * to the Kolkrabbi-backed repositories service when the feature is disabled,
13
+ * the feature lookup fails, or Repositories API cannot return a usable name.
14
+ */
15
+ export declare function resolveRepoName(ctx: ResolveRepoNameCtx, pipelineId: string, options?: ResolveRepoNameOptions): Promise<string>;
16
+ export {};
@@ -0,0 +1,54 @@
1
+ import { debug } from './debug.js';
2
+ /**
3
+ * Resolves the GitHub repository name for a canonical pipeline ID.
4
+ * Resolve user-supplied pipeline names before calling this helper.
5
+ * Uses Repositories API when the account feature is enabled, and falls back
6
+ * to the Kolkrabbi-backed repositories service when the feature is disabled,
7
+ * the feature lookup fails, or Repositories API cannot return a usable name.
8
+ */
9
+ export async function resolveRepoName(ctx, pipelineId, options = {}) {
10
+ const { signal } = options;
11
+ signal?.throwIfAborted();
12
+ const platform = signal ? ctx.platform.withOptions({ signal }) : ctx.platform;
13
+ let useRepositoriesApi;
14
+ try {
15
+ const feature = await platform.accountFeature.info('dashboard-repositories-api');
16
+ signal?.throwIfAborted();
17
+ useRepositoriesApi = feature.enabled;
18
+ }
19
+ catch (error) {
20
+ signal?.throwIfAborted();
21
+ debug('feature lookup failed error=%s; falling back', describeError(error));
22
+ }
23
+ if (useRepositoriesApi) {
24
+ try {
25
+ const repositoriesApi = signal ? ctx.repositoriesApi.withOptions({ signal }) : ctx.repositoriesApi;
26
+ const repository = await repositoriesApi.githubRepository.info(pipelineId);
27
+ signal?.throwIfAborted();
28
+ const repositoryName = repository.full_name?.trim();
29
+ if (repositoryName) {
30
+ return repositoryName;
31
+ }
32
+ debug('repositories API returned no full name; falling back');
33
+ }
34
+ catch (error) {
35
+ signal?.throwIfAborted();
36
+ debug('repositories API lookup failed error=%s; falling back', describeError(error));
37
+ }
38
+ }
39
+ else if (useRepositoriesApi === false) {
40
+ signal?.throwIfAborted();
41
+ debug('repositories API disabled; falling back');
42
+ }
43
+ const repositories = signal ? ctx.repositories.withOptions({ signal }) : ctx.repositories;
44
+ const pipelineRepository = await repositories.pipelineRepository.info(pipelineId);
45
+ signal?.throwIfAborted();
46
+ const repositoryName = pipelineRepository.repository.name.trim();
47
+ if (!repositoryName) {
48
+ throw new Error('Repositories service returned no repository name');
49
+ }
50
+ return repositoryName;
51
+ }
52
+ function describeError(error) {
53
+ return error instanceof Error ? error.name : typeof error;
54
+ }
@@ -0,0 +1,5 @@
1
+ import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
2
+ import type { HerokuClient } from '@heroku/types/repositories-api';
3
+ import type { RoutesClientExtras } from '../core/create-client.js';
4
+ export type RepositoriesApiClient = HerokuClient & RoutesClientExtras<HerokuClient>;
5
+ export declare function createRepositoriesApiClient(options?: HerokuApiClientOptions): RepositoriesApiClient;
@@ -0,0 +1,10 @@
1
+ import * as routes from '@heroku/types/repositories-api/routes';
2
+ import { createClient } from '../core/create-client.js';
3
+ const REPOSITORIES_API_ACCEPT = 'application/vnd.heroku+json; version=3.repositories-api';
4
+ export function createRepositoriesApiClient(options = {}) {
5
+ return createClient(routes, {
6
+ defaultAccept: REPOSITORIES_API_ACCEPT,
7
+ service: 'platform',
8
+ ...options,
9
+ });
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroku/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Heroku SDK",
5
5
  "author": "Heroku",
6
6
  "type": "module",
@@ -30,6 +30,10 @@
30
30
  "types": "./dist/services/repositories.d.ts",
31
31
  "default": "./dist/services/repositories.js"
32
32
  },
33
+ "./repositories-api": {
34
+ "types": "./dist/services/repositories-api.d.ts",
35
+ "default": "./dist/services/repositories-api.js"
36
+ },
33
37
  "./extensions/platform": {
34
38
  "types": "./dist/resources/extensions/platform.d.ts",
35
39
  "default": "./dist/resources/extensions/platform.js"
@@ -70,6 +74,10 @@
70
74
  "types": "./dist/resources/platform/pipeline/*.d.ts",
71
75
  "default": "./dist/resources/platform/pipeline/*.js"
72
76
  },
77
+ "./resources/platform/review-app-config": {
78
+ "types": "./dist/resources/platform/review-app-config/index.d.ts",
79
+ "default": "./dist/resources/platform/review-app-config/index.js"
80
+ },
73
81
  "./resources/platform/sni-endpoint": {
74
82
  "types": "./dist/resources/platform/sni-endpoint/index.d.ts",
75
83
  "default": "./dist/resources/platform/sni-endpoint/index.js"
@@ -108,7 +116,8 @@
108
116
  "lint:fix": "eslint --fix",
109
117
  "prebuild": "rimraf dist",
110
118
  "prepare": "npm run build",
111
- "test": "vitest --run"
119
+ "test": "vitest --run",
120
+ "test:types": "tsc --noEmit -p tsconfig.type-tests.json"
112
121
  },
113
122
  "license": "Apache-2.0",
114
123
  "engines": {
@@ -132,7 +141,7 @@
132
141
  },
133
142
  "dependencies": {
134
143
  "@heroku/heroku-fetch": "^0.1.4",
135
- "@heroku/types": "^4.3.0",
144
+ "@heroku/types": "^4.7.0",
136
145
  "debug": "^4.4.0"
137
146
  }
138
147
  }