@heroku/sdk 0.5.5 → 0.5.7
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 +4 -1
- package/dist/core/dispatcher.js +16 -2
- package/dist/core/extend-resource.d.ts +3 -1
- package/dist/core/heroku-sdk.d.ts +2 -0
- package/dist/core/heroku-sdk.js +15 -0
- package/dist/resources/platform/app/create-and-setup.d.ts +50 -0
- package/dist/resources/platform/app/create-and-setup.js +60 -0
- package/dist/resources/platform/app/index.d.ts +6 -0
- package/dist/resources/platform/app/index.js +6 -0
- package/dist/resources/platform/app/transfer.d.ts +23 -0
- package/dist/resources/platform/app/transfer.js +35 -0
- package/dist/resources/platform/domain/test-utils.js +1 -0
- package/dist/services/metrics.d.ts +5 -0
- package/dist/services/metrics.js +23 -0
- package/dist/services/platform.js +15 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Heroku SDK
|
|
2
2
|
|
|
3
|
-
A TypeScript SDK for the [Heroku Platform API](https://devcenter.heroku.com/articles/platform-api-reference)
|
|
3
|
+
A TypeScript SDK for the [Heroku Platform API](https://devcenter.heroku.com/articles/platform-api-reference), [Heroku Data API](https://devcenter.heroku.com/articles/heroku-postgres-api-reference), and the Heroku Metrics API. It generates fully-typed clients at runtime from route definitions — no hand-written method per endpoint.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -40,9 +40,11 @@ If you only need one service (and want the smallest possible bundle), import the
|
|
|
40
40
|
```ts
|
|
41
41
|
import { createPlatformClient } from '@heroku/sdk/platform'
|
|
42
42
|
import { createDataClient } from '@heroku/sdk/data'
|
|
43
|
+
import { createMetricsClient } from '@heroku/sdk/metrics'
|
|
43
44
|
|
|
44
45
|
const platform = createPlatformClient()
|
|
45
46
|
const data = createDataClient()
|
|
47
|
+
const metrics = createMetricsClient()
|
|
46
48
|
|
|
47
49
|
const apps = await platform.app.list()
|
|
48
50
|
```
|
|
@@ -54,6 +56,7 @@ const apps = await platform.app.list()
|
|
|
54
56
|
| `HerokuSDK`, `extendResource`, types | `@heroku/sdk` |
|
|
55
57
|
| `createPlatformClient`, `PlatformClient` | `@heroku/sdk/platform` |
|
|
56
58
|
| `createDataClient`, `DataClient` | `@heroku/sdk/data` |
|
|
59
|
+
| `createMetricsClient`, `MetricsClient` | `@heroku/sdk/metrics` |
|
|
57
60
|
| `appExtensions`, `dynoExtensions`, … | `@heroku/sdk/extensions/platform` |
|
|
58
61
|
| `databaseExtensions`, … | `@heroku/sdk/extensions/data` |
|
|
59
62
|
|
package/dist/core/dispatcher.js
CHANGED
|
@@ -16,8 +16,22 @@ export async function dispatch(client, route, args, invocation, requestOptions)
|
|
|
16
16
|
if (route.hasRequestBody && remaining.length > 0) {
|
|
17
17
|
body = remaining[0];
|
|
18
18
|
}
|
|
19
|
+
let effectiveOptions = requestOptions;
|
|
20
|
+
if (route.query && route.query.length > 0 && remaining.length > 0) {
|
|
21
|
+
const raw = remaining[0];
|
|
22
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
23
|
+
const searchParams = {};
|
|
24
|
+
for (const key of route.query) {
|
|
25
|
+
const value = raw[key];
|
|
26
|
+
if (value !== undefined && value !== null) {
|
|
27
|
+
searchParams[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
effectiveOptions = { ...requestOptions, searchParams };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
19
33
|
debug('%s %s %s -> %s hasBody=%s', invocation ?? 'dispatch', route.method, route.path, path, body !== undefined);
|
|
20
|
-
const response = await callMethod(client, route.method, path, body,
|
|
34
|
+
const response = await callMethod(client, route.method, path, body, effectiveOptions);
|
|
21
35
|
const contentLength = response.headers.get('content-length');
|
|
22
36
|
if (response.status === 204 || contentLength === '0') {
|
|
23
37
|
debug('%s empty response status=%d content-length=%s', invocation ?? 'dispatch', response.status, contentLength ?? '');
|
|
@@ -30,7 +44,7 @@ export async function dispatch(client, route, args, invocation, requestOptions)
|
|
|
30
44
|
const nextRange = response.headers.get('next-range');
|
|
31
45
|
if (!nextRange)
|
|
32
46
|
return result;
|
|
33
|
-
return followPages(client, path, result, nextRange, invocation,
|
|
47
|
+
return followPages(client, path, result, nextRange, invocation, effectiveOptions);
|
|
34
48
|
}
|
|
35
49
|
/* eslint-disable no-await-in-loop -- pagination is inherently sequential; each page depends on the previous next-range header */
|
|
36
50
|
async function followPages(client, path, firstPage, initialRange, invocation, requestOptions) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { DataClient } from '../services/data.js';
|
|
2
|
+
import type { MetricsClient } from '../services/metrics.js';
|
|
2
3
|
import type { PlatformClient } from '../services/platform.js';
|
|
3
|
-
export type ServiceName = 'data' | 'platform';
|
|
4
|
+
export type ServiceName = 'data' | 'metrics' | 'platform';
|
|
4
5
|
export type ResourceCtx = {
|
|
5
6
|
data: DataClient;
|
|
7
|
+
metrics: MetricsClient;
|
|
6
8
|
platform: PlatformClient;
|
|
7
9
|
};
|
|
8
10
|
export type ResourceMethods = Record<string, (...args: any[]) => any>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
2
2
|
import type { DataClient } from '../services/data.js';
|
|
3
|
+
import type { MetricsClient } from '../services/metrics.js';
|
|
3
4
|
import type { PlatformClient } from '../services/platform.js';
|
|
4
5
|
import type { ApplyExtensions, ExtensionsFor, ResourceExtension } from './extend-resource.js';
|
|
5
6
|
export type HerokuSDKOptions<Exts extends readonly ResourceExtension[]> = {
|
|
@@ -10,5 +11,6 @@ export declare class HerokuSDK<const Exts extends readonly ResourceExtension[] =
|
|
|
10
11
|
#private;
|
|
11
12
|
constructor(options?: HerokuSDKOptions<Exts>);
|
|
12
13
|
get data(): ApplyExtensions<DataClient, ExtensionsFor<Exts, 'data'>>;
|
|
14
|
+
get metrics(): ApplyExtensions<MetricsClient, ExtensionsFor<Exts, 'metrics'>>;
|
|
13
15
|
get platform(): ApplyExtensions<PlatformClient, ExtensionsFor<Exts, 'platform'>>;
|
|
14
16
|
}
|
package/dist/core/heroku-sdk.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createDataClient } from '../services/data.js';
|
|
2
|
+
import { createMetricsClient } from '../services/metrics.js';
|
|
2
3
|
import { createPlatformClient } from '../services/platform.js';
|
|
3
4
|
import { mergeExtensions } from './extensions-proxy.js';
|
|
4
5
|
function partitionByService(extensions) {
|
|
@@ -15,8 +16,10 @@ export class HerokuSDK {
|
|
|
15
16
|
#ctx;
|
|
16
17
|
#data;
|
|
17
18
|
#extensionsByService;
|
|
19
|
+
#metrics;
|
|
18
20
|
#platform;
|
|
19
21
|
#rawData;
|
|
22
|
+
#rawMetrics;
|
|
20
23
|
#rawPlatform;
|
|
21
24
|
constructor(options = {}) {
|
|
22
25
|
this.#clientOptions = options.clientOptions ?? {};
|
|
@@ -26,6 +29,10 @@ export class HerokuSDK {
|
|
|
26
29
|
this.#data ??= mergeExtensions(this.#getRawData(), this.#extensionsByService.get('data') ?? [], this.#getCtx());
|
|
27
30
|
return this.#data;
|
|
28
31
|
}
|
|
32
|
+
get metrics() {
|
|
33
|
+
this.#metrics ??= mergeExtensions(this.#getRawMetrics(), this.#extensionsByService.get('metrics') ?? [], this.#getCtx());
|
|
34
|
+
return this.#metrics;
|
|
35
|
+
}
|
|
29
36
|
get platform() {
|
|
30
37
|
this.#platform ??= mergeExtensions(this.#getRawPlatform(), this.#extensionsByService.get('platform') ?? [], this.#getCtx());
|
|
31
38
|
return this.#platform;
|
|
@@ -36,6 +43,10 @@ export class HerokuSDK {
|
|
|
36
43
|
enumerable: true,
|
|
37
44
|
get: () => this.#getRawData(),
|
|
38
45
|
},
|
|
46
|
+
metrics: {
|
|
47
|
+
enumerable: true,
|
|
48
|
+
get: () => this.#getRawMetrics(),
|
|
49
|
+
},
|
|
39
50
|
platform: {
|
|
40
51
|
enumerable: true,
|
|
41
52
|
get: () => this.#getRawPlatform(),
|
|
@@ -47,6 +58,10 @@ export class HerokuSDK {
|
|
|
47
58
|
this.#rawData ??= createDataClient(this.#clientOptions);
|
|
48
59
|
return this.#rawData;
|
|
49
60
|
}
|
|
61
|
+
#getRawMetrics() {
|
|
62
|
+
this.#rawMetrics ??= createMetricsClient(this.#clientOptions);
|
|
63
|
+
return this.#rawMetrics;
|
|
64
|
+
}
|
|
50
65
|
#getRawPlatform() {
|
|
51
66
|
this.#rawPlatform ??= createPlatformClient(this.#clientOptions);
|
|
52
67
|
return this.#rawPlatform;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { App, ConfigVarUpdateOpts } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
export type SetupStep = {
|
|
4
|
+
kind: 'addon' | 'buildpack' | 'config-vars' | 'setup';
|
|
5
|
+
label?: string;
|
|
6
|
+
};
|
|
7
|
+
export type CreateAndSetupOptions = {
|
|
8
|
+
poller?: {
|
|
9
|
+
onStart?: (step: SetupStep) => void;
|
|
10
|
+
onStop?: (step?: SetupStep) => void;
|
|
11
|
+
};
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
};
|
|
14
|
+
export type CreateAndSetupInput = {
|
|
15
|
+
addons?: Array<{
|
|
16
|
+
as?: string;
|
|
17
|
+
plan: string;
|
|
18
|
+
}>;
|
|
19
|
+
buildpack?: string;
|
|
20
|
+
configVars?: ConfigVarUpdateOpts;
|
|
21
|
+
name?: string;
|
|
22
|
+
region?: string;
|
|
23
|
+
space?: string;
|
|
24
|
+
stack?: string;
|
|
25
|
+
team?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Create an app and run its post-create setup in one call.
|
|
29
|
+
*
|
|
30
|
+
* Creates the app (team/space → `teamApp.create`, otherwise `app.create`),
|
|
31
|
+
* then fans out the optional setup steps in parallel: add-ons
|
|
32
|
+
* (`addOn.create` per plan), config vars (`configVar.update`), and buildpack
|
|
33
|
+
* (`buildpackInstallation.update`). Returns the created `App`.
|
|
34
|
+
*
|
|
35
|
+
* Progress is reported via the `poller` convention. Because the steps run in
|
|
36
|
+
* parallel and a caller's progress indicator is typically a single global
|
|
37
|
+
* spinner, this method fires a single `onStart`/`onStop` pair around the whole
|
|
38
|
+
* setup batch rather than one per step, so parallel steps don't clobber the
|
|
39
|
+
* spinner. Per the poller contract, `onStart` fires BEFORE any request is
|
|
40
|
+
* dispatched, and `onStop` fires ONLY on success — if a step rejects, the
|
|
41
|
+
* error propagates and `onStop` is never called (there is no try/finally, so
|
|
42
|
+
* the caller's error path stops the spinner). Signal threading uses the
|
|
43
|
+
* standard `withOptions({signal})`
|
|
44
|
+
* scoped-client idiom so an abort cancels in-flight requests, not just the
|
|
45
|
+
* pre-flight `throwIfAborted()`.
|
|
46
|
+
*
|
|
47
|
+
* The CLI parses flags / heroku.yml into `input` and owns git-remote setup
|
|
48
|
+
* and rendering; this method owns the API orchestration only.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createAndSetup(ctx: Pick<ResourceCtx, 'platform'>, input: CreateAndSetupInput, options?: CreateAndSetupOptions): Promise<App>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create an app and run its post-create setup in one call.
|
|
3
|
+
*
|
|
4
|
+
* Creates the app (team/space → `teamApp.create`, otherwise `app.create`),
|
|
5
|
+
* then fans out the optional setup steps in parallel: add-ons
|
|
6
|
+
* (`addOn.create` per plan), config vars (`configVar.update`), and buildpack
|
|
7
|
+
* (`buildpackInstallation.update`). Returns the created `App`.
|
|
8
|
+
*
|
|
9
|
+
* Progress is reported via the `poller` convention. Because the steps run in
|
|
10
|
+
* parallel and a caller's progress indicator is typically a single global
|
|
11
|
+
* spinner, this method fires a single `onStart`/`onStop` pair around the whole
|
|
12
|
+
* setup batch rather than one per step, so parallel steps don't clobber the
|
|
13
|
+
* spinner. Per the poller contract, `onStart` fires BEFORE any request is
|
|
14
|
+
* dispatched, and `onStop` fires ONLY on success — if a step rejects, the
|
|
15
|
+
* error propagates and `onStop` is never called (there is no try/finally, so
|
|
16
|
+
* the caller's error path stops the spinner). Signal threading uses the
|
|
17
|
+
* standard `withOptions({signal})`
|
|
18
|
+
* scoped-client idiom so an abort cancels in-flight requests, not just the
|
|
19
|
+
* pre-flight `throwIfAborted()`.
|
|
20
|
+
*
|
|
21
|
+
* The CLI parses flags / heroku.yml into `input` and owns git-remote setup
|
|
22
|
+
* and rendering; this method owns the API orchestration only.
|
|
23
|
+
*/
|
|
24
|
+
export async function createAndSetup(ctx, input, options = {}) {
|
|
25
|
+
options.signal?.throwIfAborted();
|
|
26
|
+
const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
|
|
27
|
+
const { addons, buildpack, configVars, ...createParams } = input;
|
|
28
|
+
const app = (createParams.space || createParams.team)
|
|
29
|
+
? (await platform.teamApp.create(createParams))
|
|
30
|
+
: await platform.app.create(createParams);
|
|
31
|
+
if (!app.name)
|
|
32
|
+
throw new Error('createAndSetup: created app has no name');
|
|
33
|
+
const appName = app.name;
|
|
34
|
+
// Deferred thunks: nothing is dispatched until the batch is launched below,
|
|
35
|
+
// so `onStart` reliably fires before the first request.
|
|
36
|
+
const steps = [];
|
|
37
|
+
if (addons?.length) {
|
|
38
|
+
for (const addon of addons) {
|
|
39
|
+
steps.push(() => platform.addOn.create(appName, {
|
|
40
|
+
attachment: addon.as ? { name: addon.as } : undefined,
|
|
41
|
+
plan: addon.plan,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (configVars && Object.keys(configVars).length > 0) {
|
|
46
|
+
steps.push(() => platform.configVar.update(appName, configVars));
|
|
47
|
+
}
|
|
48
|
+
if (buildpack) {
|
|
49
|
+
steps.push(() => platform.buildpackInstallation.update(appName, { updates: [{ buildpack }] }));
|
|
50
|
+
}
|
|
51
|
+
if (steps.length > 0) {
|
|
52
|
+
// Single combined progress window — one spinner for the whole parallel
|
|
53
|
+
// setup batch, not one per step. onStart before dispatch; onStop only on
|
|
54
|
+
// success (no try/finally — a rejection skips onStop per contract).
|
|
55
|
+
options.poller?.onStart?.({ kind: 'setup', label: 'setup' });
|
|
56
|
+
await Promise.all(steps.map(step => step()));
|
|
57
|
+
options.poller?.onStop?.();
|
|
58
|
+
}
|
|
59
|
+
return app;
|
|
60
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { App } from '@heroku/types/3.sdk';
|
|
2
2
|
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
import type { CreateAndSetupInput, CreateAndSetupOptions } from './create-and-setup.js';
|
|
3
4
|
import type { DescribeAppOptions } from './info.js';
|
|
5
|
+
import type { TransferOptions } from './transfer.js';
|
|
4
6
|
import type { WaitForACMCertificatesOptions } from './wait-for-acm-certificates.js';
|
|
7
|
+
export { createAndSetup, type CreateAndSetupInput, type CreateAndSetupOptions, type SetupStep, } from './create-and-setup.js';
|
|
5
8
|
export { type AppInfo, describeApp, type DescribeAppOptions, type PipelineCouplingDetail, } from './info.js';
|
|
9
|
+
export { transferApp, type TransferOptions } from './transfer.js';
|
|
6
10
|
export { waitForACMCertificates, type WaitForACMCertificatesOptions } from './wait-for-acm-certificates.js';
|
|
7
11
|
export type AppMaintenanceOptions = {
|
|
8
12
|
signal?: AbortSignal;
|
|
@@ -51,11 +55,13 @@ export declare function getProcessTier(ctx: {
|
|
|
51
55
|
platform: AppInfoPlatform;
|
|
52
56
|
}, appIdentity: string, options?: GetProcessTierOptions): Promise<ProcessTier | undefined>;
|
|
53
57
|
export declare const appExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "app", {
|
|
58
|
+
createAndSetup: (input: CreateAndSetupInput, options?: CreateAndSetupOptions) => Promise<App>;
|
|
54
59
|
describe: (appIdentity: string, options?: DescribeAppOptions) => Promise<import("./info.js").AppInfo>;
|
|
55
60
|
disableMaintenance: (appIdentity: string, options?: AppMaintenanceOptions) => Promise<App>;
|
|
56
61
|
enableMaintenance: (appIdentity: string, options?: AppMaintenanceOptions) => Promise<App>;
|
|
57
62
|
getGeneration: (appIdentity: string, options?: GetGenerationOptions) => Promise<GenerationKind | undefined>;
|
|
58
63
|
getProcessTier: (appIdentity: string, options?: GetProcessTierOptions) => Promise<ProcessTier | undefined>;
|
|
59
64
|
isShielded: (appIdentity: string, options?: IsShieldedOptions) => Promise<boolean>;
|
|
65
|
+
transfer: (appIdentity: string, recipient: string, options?: TransferOptions) => Promise<import("@heroku/types/3.sdk").TeamApp | import("@heroku/types/3.sdk").AppTransfer>;
|
|
60
66
|
waitForACMCertificates: (appIdentity: string, options?: WaitForACMCertificatesOptions) => Promise<import("@heroku/types/3.sdk").Domain[]>;
|
|
61
67
|
}>;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { createAndSetup } from './create-and-setup.js';
|
|
2
3
|
import { describeApp } from './info.js';
|
|
4
|
+
import { transferApp } from './transfer.js';
|
|
3
5
|
import { waitForACMCertificates } from './wait-for-acm-certificates.js';
|
|
6
|
+
export { createAndSetup, } from './create-and-setup.js';
|
|
4
7
|
export { describeApp, } from './info.js';
|
|
8
|
+
export { transferApp } from './transfer.js';
|
|
5
9
|
export { waitForACMCertificates } from './wait-for-acm-certificates.js';
|
|
6
10
|
export async function enableMaintenance(ctx, appIdentity, options = {}) {
|
|
7
11
|
options.signal?.throwIfAborted();
|
|
@@ -46,11 +50,13 @@ export async function getProcessTier(ctx, appIdentity, options = {}) {
|
|
|
46
50
|
return app.process_tier;
|
|
47
51
|
}
|
|
48
52
|
export const appExtensions = extendResource('platform', 'app', ctx => ({
|
|
53
|
+
createAndSetup: (input, options) => createAndSetup(ctx, input, options),
|
|
49
54
|
describe: (appIdentity, options) => describeApp(ctx, appIdentity, options),
|
|
50
55
|
disableMaintenance: (appIdentity, options) => disableMaintenance(ctx, appIdentity, options),
|
|
51
56
|
enableMaintenance: (appIdentity, options) => enableMaintenance(ctx, appIdentity, options),
|
|
52
57
|
getGeneration: (appIdentity, options) => getGeneration(ctx, appIdentity, options),
|
|
53
58
|
getProcessTier: (appIdentity, options) => getProcessTier(ctx, appIdentity, options),
|
|
54
59
|
isShielded: (appIdentity, options) => isShielded(ctx, appIdentity, options),
|
|
60
|
+
transfer: (appIdentity, recipient, options) => transferApp(ctx, appIdentity, recipient, options),
|
|
55
61
|
waitForACMCertificates: (appIdentity, options) => waitForACMCertificates(ctx, appIdentity, options),
|
|
56
62
|
}));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AppTransfer, TeamApp } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
export type TransferOptions = {
|
|
4
|
+
personalToPersonal?: boolean;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
silent?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Transfer an app to another user or team.
|
|
10
|
+
*
|
|
11
|
+
* Route selection mirrors the platform's two transfer surfaces:
|
|
12
|
+
* - personal-to-personal transfers go through `POST /account/app-transfers`
|
|
13
|
+
* (`appTransfer.create`), which emails the recipient for acceptance
|
|
14
|
+
* (response `state: 'pending'`).
|
|
15
|
+
* - team-involved transfers go through `PATCH /teams/apps/{name}`
|
|
16
|
+
* (`teamApp.transferToTeam` / `transferToAccount`), differing only by body.
|
|
17
|
+
*
|
|
18
|
+
* The caller (CLI) decides `personalToPersonal` and interprets the returned
|
|
19
|
+
* `state` for presentation. `personalToPersonal` defaults falsy → the team
|
|
20
|
+
* route, so a caller who forgets the flag on a genuine personal transfer
|
|
21
|
+
* silently hits the team path (a caller-owned footgun the SDK can't detect).
|
|
22
|
+
*/
|
|
23
|
+
export declare function transferApp(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, recipient: string, options?: TransferOptions): Promise<AppTransfer | TeamApp>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transfer an app to another user or team.
|
|
3
|
+
*
|
|
4
|
+
* Route selection mirrors the platform's two transfer surfaces:
|
|
5
|
+
* - personal-to-personal transfers go through `POST /account/app-transfers`
|
|
6
|
+
* (`appTransfer.create`), which emails the recipient for acceptance
|
|
7
|
+
* (response `state: 'pending'`).
|
|
8
|
+
* - team-involved transfers go through `PATCH /teams/apps/{name}`
|
|
9
|
+
* (`teamApp.transferToTeam` / `transferToAccount`), differing only by body.
|
|
10
|
+
*
|
|
11
|
+
* The caller (CLI) decides `personalToPersonal` and interprets the returned
|
|
12
|
+
* `state` for presentation. `personalToPersonal` defaults falsy → the team
|
|
13
|
+
* route, so a caller who forgets the flag on a genuine personal transfer
|
|
14
|
+
* silently hits the team path (a caller-owned footgun the SDK can't detect).
|
|
15
|
+
*/
|
|
16
|
+
export async function transferApp(ctx, appIdentity, recipient, options = {}) {
|
|
17
|
+
options.signal?.throwIfAborted();
|
|
18
|
+
const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
|
|
19
|
+
if (options.personalToPersonal) {
|
|
20
|
+
const body = { app: appIdentity, recipient };
|
|
21
|
+
if (options.silent !== undefined)
|
|
22
|
+
body.silent = options.silent;
|
|
23
|
+
return platform.appTransfer.create(body);
|
|
24
|
+
}
|
|
25
|
+
// Team-involved (personalToPersonal=false): both routes are PATCH /teams/apps/{name}
|
|
26
|
+
// with body {owner}, differing only by semantics. Split by recipient shape: an
|
|
27
|
+
// email recipient transfers ownership to a personal ACCOUNT; anything else is a
|
|
28
|
+
// TEAM name. Both produce a byte-identical wire request to the legacy
|
|
29
|
+
// app-transfer.ts (a single PATCH with {owner: recipient}), so this is a
|
|
30
|
+
// spec-faithful split with no functional regression.
|
|
31
|
+
const looksLikeEmail = recipient.includes('@');
|
|
32
|
+
return looksLikeEmail
|
|
33
|
+
? platform.teamApp.transferToAccount(appIdentity, { owner: recipient })
|
|
34
|
+
: platform.teamApp.transferToTeam(appIdentity, { owner: recipient });
|
|
35
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
2
|
+
import type { HerokuClient } from '@heroku/types/metrics';
|
|
3
|
+
import type { RoutesClientExtras } from '../core/create-client.js';
|
|
4
|
+
export type MetricsClient = HerokuClient & RoutesClientExtras<HerokuClient>;
|
|
5
|
+
export declare function createMetricsClient(options?: HerokuApiClientOptions): MetricsClient;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as routes from '@heroku/types/metrics/routes';
|
|
2
|
+
import { createClient } from '../core/create-client.js';
|
|
3
|
+
const DEFAULT_METRICS_BASE_URL = 'https://api.metrics.heroku.com';
|
|
4
|
+
export function createMetricsClient(options = {}) {
|
|
5
|
+
const baseUrl = process.env.HEROKU_METRICS_HOST ?? DEFAULT_METRICS_BASE_URL;
|
|
6
|
+
// Metrics is a distinct host that heroku-fetch does not model as a first-class
|
|
7
|
+
// service, so we use `service: 'custom'` with an explicit base URL. Auth (bearer
|
|
8
|
+
// token) is resolved by heroku-fetch exactly as for platform/data — same
|
|
9
|
+
// HEROKU_API_KEY / netrc source, shared across services on one SDK instance.
|
|
10
|
+
//
|
|
11
|
+
// Unlike platform/data, `service: 'custom'` sends no default Accept header, so we
|
|
12
|
+
// set `application/vnd.heroku+json; version=3` explicitly to match what meetas
|
|
13
|
+
// expects — the same versioned vendor type platform/data get automatically via
|
|
14
|
+
// heroku-fetch's `defaultAccept`. A caller can still override it via `headers`:
|
|
15
|
+
// `...options` first, then an explicit `headers` key whose `...options.headers`
|
|
16
|
+
// spread lands AFTER the literal Accept, so caller headers win for any key they set.
|
|
17
|
+
return createClient(routes, {
|
|
18
|
+
baseUrl,
|
|
19
|
+
service: 'custom',
|
|
20
|
+
...options,
|
|
21
|
+
headers: { Accept: 'application/vnd.heroku+json; version=3', ...options.headers },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import * as routes from '@heroku/types/3.sdk/routes';
|
|
2
2
|
import { createClient } from '../core/create-client.js';
|
|
3
|
+
/**
|
|
4
|
+
* The Platform API variant this client's routes and types are generated from
|
|
5
|
+
* (`@heroku/types/3.sdk`). Dispatch at the same variant so the wire matches the
|
|
6
|
+
* shipped types — otherwise `3.sdk`-only routes (e.g. `buildMetadata.info`) 404
|
|
7
|
+
* under heroku-fetch's shared `version=3` default, and fields the types describe
|
|
8
|
+
* in their `3.sdk` shape (e.g. `App.generation: string`) arrive in the older
|
|
9
|
+
* `version=3` shape. A caller can still override per request via `withHeaders`,
|
|
10
|
+
* or per client by passing their own `defaultAccept`.
|
|
11
|
+
*/
|
|
12
|
+
const PLATFORM_SDK_ACCEPT = 'application/vnd.heroku+json; version=3.sdk';
|
|
3
13
|
export function createPlatformClient(options = {}) {
|
|
4
|
-
return createClient(routes, {
|
|
14
|
+
return createClient(routes, {
|
|
15
|
+
defaultAccept: PLATFORM_SDK_ACCEPT,
|
|
16
|
+
service: 'platform',
|
|
17
|
+
...options,
|
|
18
|
+
});
|
|
5
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroku/sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Heroku SDK",
|
|
5
5
|
"author": "Heroku",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"types": "./dist/services/data.d.ts",
|
|
19
19
|
"default": "./dist/services/data.js"
|
|
20
20
|
},
|
|
21
|
+
"./metrics": {
|
|
22
|
+
"types": "./dist/services/metrics.d.ts",
|
|
23
|
+
"default": "./dist/services/metrics.js"
|
|
24
|
+
},
|
|
21
25
|
"./extensions/platform": {
|
|
22
26
|
"types": "./dist/resources/extensions/platform.d.ts",
|
|
23
27
|
"default": "./dist/resources/extensions/platform.js"
|
|
@@ -99,8 +103,8 @@
|
|
|
99
103
|
"vitest": "^4.1.4"
|
|
100
104
|
},
|
|
101
105
|
"dependencies": {
|
|
102
|
-
"@heroku/heroku-fetch": "^0.1.
|
|
103
|
-
"@heroku/types": "^4.0
|
|
106
|
+
"@heroku/heroku-fetch": "^0.1.4",
|
|
107
|
+
"@heroku/types": "^4.1.0",
|
|
104
108
|
"debug": "^4.4.0"
|
|
105
109
|
}
|
|
106
110
|
}
|