@heroku/sdk 0.1.0-alpha.0

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 (109) hide show
  1. package/LICENSE.txt +207 -0
  2. package/README.md +94 -0
  3. package/dist/compositions/add-on.d.ts +7 -0
  4. package/dist/compositions/add-on.js +6 -0
  5. package/dist/compositions/app.d.ts +8 -0
  6. package/dist/compositions/app.js +11 -0
  7. package/dist/compositions/dyno.d.ts +17 -0
  8. package/dist/compositions/dyno.js +23 -0
  9. package/dist/compositions/pg.d.ts +15 -0
  10. package/dist/compositions/pg.js +56 -0
  11. package/dist/compositions/pipeline.d.ts +33 -0
  12. package/dist/compositions/pipeline.js +146 -0
  13. package/dist/core/create-client.d.ts +32 -0
  14. package/dist/core/create-client.js +48 -0
  15. package/dist/core/dispatcher.d.ts +3 -0
  16. package/dist/core/dispatcher.js +80 -0
  17. package/dist/core/extend-resource.d.ts +24 -0
  18. package/dist/core/extend-resource.js +3 -0
  19. package/dist/core/extensions-proxy.d.ts +2 -0
  20. package/dist/core/extensions-proxy.js +56 -0
  21. package/dist/core/heroku-sdk.d.ts +14 -0
  22. package/dist/core/heroku-sdk.js +54 -0
  23. package/dist/core/interpolate-path.d.ts +3 -0
  24. package/dist/core/interpolate-path.js +13 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +2 -0
  27. package/dist/resources/data/backup.d.ts +9 -0
  28. package/dist/resources/data/backup.js +10 -0
  29. package/dist/resources/data/database.d.ts +54 -0
  30. package/dist/resources/data/database.js +44 -0
  31. package/dist/resources/data/internal/resolve-addon-id.d.ts +2 -0
  32. package/dist/resources/data/internal/resolve-addon-id.js +13 -0
  33. package/dist/resources/data/internal/resolve-pg-database.d.ts +23 -0
  34. package/dist/resources/data/internal/resolve-pg-database.js +52 -0
  35. package/dist/resources/data/maintenance.d.ts +9 -0
  36. package/dist/resources/data/maintenance.js +10 -0
  37. package/dist/resources/data/postgres-credential.d.ts +11 -0
  38. package/dist/resources/data/postgres-credential.js +16 -0
  39. package/dist/resources/data/postgres-database.d.ts +32 -0
  40. package/dist/resources/data/postgres-database.js +14 -0
  41. package/dist/resources/data/postgres.d.ts +9 -0
  42. package/dist/resources/data/postgres.js +10 -0
  43. package/dist/resources/data/transfer-schedule.d.ts +9 -0
  44. package/dist/resources/data/transfer-schedule.js +10 -0
  45. package/dist/resources/data/transfer.d.ts +49 -0
  46. package/dist/resources/data/transfer.js +14 -0
  47. package/dist/resources/extensions/data.d.ts +4 -0
  48. package/dist/resources/extensions/data.js +4 -0
  49. package/dist/resources/extensions/platform.d.ts +8 -0
  50. package/dist/resources/extensions/platform.js +8 -0
  51. package/dist/resources/platform/add-on/create-and-wait.d.ts +28 -0
  52. package/dist/resources/platform/add-on/create-and-wait.js +63 -0
  53. package/dist/resources/platform/add-on/debug.d.ts +2 -0
  54. package/dist/resources/platform/add-on/debug.js +2 -0
  55. package/dist/resources/platform/add-on/describe.d.ts +20 -0
  56. package/dist/resources/platform/add-on/describe.js +51 -0
  57. package/dist/resources/platform/add-on/errors.d.ts +51 -0
  58. package/dist/resources/platform/add-on/errors.js +61 -0
  59. package/dist/resources/platform/add-on/index.d.ts +23 -0
  60. package/dist/resources/platform/add-on/index.js +25 -0
  61. package/dist/resources/platform/add-on/list-plans.d.ts +34 -0
  62. package/dist/resources/platform/add-on/list-plans.js +53 -0
  63. package/dist/resources/platform/add-on/pricing.d.ts +72 -0
  64. package/dist/resources/platform/add-on/pricing.js +100 -0
  65. package/dist/resources/platform/add-on/resolve.d.ts +43 -0
  66. package/dist/resources/platform/add-on/resolve.js +103 -0
  67. package/dist/resources/platform/add-on/types.d.ts +49 -0
  68. package/dist/resources/platform/add-on/types.js +1 -0
  69. package/dist/resources/platform/add-on/upgrade.d.ts +21 -0
  70. package/dist/resources/platform/add-on/upgrade.js +36 -0
  71. package/dist/resources/platform/add-on.d.ts +211 -0
  72. package/dist/resources/platform/add-on.js +354 -0
  73. package/dist/resources/platform/app/index.d.ts +58 -0
  74. package/dist/resources/platform/app/index.js +53 -0
  75. package/dist/resources/platform/app/info.d.ts +30 -0
  76. package/dist/resources/platform/app/info.js +27 -0
  77. package/dist/resources/platform/app.d.ts +56 -0
  78. package/dist/resources/platform/app.js +50 -0
  79. package/dist/resources/platform/dyno/index.d.ts +45 -0
  80. package/dist/resources/platform/dyno/index.js +26 -0
  81. package/dist/resources/platform/dyno.d.ts +45 -0
  82. package/dist/resources/platform/dyno.js +26 -0
  83. package/dist/resources/platform/log-session/index.d.ts +102 -0
  84. package/dist/resources/platform/log-session/index.js +223 -0
  85. package/dist/resources/platform/log-session/parser.d.ts +74 -0
  86. package/dist/resources/platform/log-session/parser.js +71 -0
  87. package/dist/resources/platform/log-session.d.ts +81 -0
  88. package/dist/resources/platform/log-session.js +167 -0
  89. package/dist/resources/platform/pipeline/coupling.d.ts +27 -0
  90. package/dist/resources/platform/pipeline/coupling.js +55 -0
  91. package/dist/resources/platform/pipeline/index.d.ts +53 -0
  92. package/dist/resources/platform/pipeline/index.js +75 -0
  93. package/dist/resources/platform/pipeline/promotion.d.ts +21 -0
  94. package/dist/resources/platform/pipeline/promotion.js +109 -0
  95. package/dist/resources/platform/pipeline-coupling.d.ts +27 -0
  96. package/dist/resources/platform/pipeline-coupling.js +55 -0
  97. package/dist/resources/platform/pipeline-promotion.d.ts +21 -0
  98. package/dist/resources/platform/pipeline-promotion.js +128 -0
  99. package/dist/resources/platform/pipeline.d.ts +53 -0
  100. package/dist/resources/platform/pipeline.js +75 -0
  101. package/dist/resources/platform/shield.d.ts +2 -0
  102. package/dist/resources/platform/shield.js +6 -0
  103. package/dist/services/data.d.ts +5 -0
  104. package/dist/services/data.js +5 -0
  105. package/dist/services/platform.d.ts +5 -0
  106. package/dist/services/platform.js +5 -0
  107. package/dist/utils/wait.d.ts +16 -0
  108. package/dist/utils/wait.js +35 -0
  109. package/package.json +72 -0
@@ -0,0 +1,48 @@
1
+ import { HerokuApiClient } from '@heroku/heroku-fetch';
2
+ import createDebug from 'debug';
3
+ import { dispatch } from './dispatcher.js';
4
+ const debug = createDebug('heroku:sdk:client');
5
+ export function createClient(routes, options = {}) {
6
+ const httpClient = new HerokuApiClient(options);
7
+ return buildClientProxy(httpClient, routes);
8
+ }
9
+ function mergeStickyOptions(base, next) {
10
+ const merged = { ...base };
11
+ if (next.headers || base?.headers) {
12
+ merged.headers = { ...base?.headers, ...next.headers };
13
+ }
14
+ // signal and timeout are not additive — last wins, but only if the
15
+ // caller actually set them. Otherwise the previous value persists.
16
+ if (next.signal !== undefined)
17
+ merged.signal = next.signal;
18
+ if (next.timeout !== undefined)
19
+ merged.timeout = next.timeout;
20
+ return merged;
21
+ }
22
+ function buildClientProxy(httpClient, routes, inheritedOptions) {
23
+ return new Proxy({}, {
24
+ get(_target, resourceKey) {
25
+ if (resourceKey === 'withOptions') {
26
+ return (options) => buildClientProxy(httpClient, routes, mergeStickyOptions(inheritedOptions, options));
27
+ }
28
+ if (resourceKey === 'withHeaders') {
29
+ return (headers) => buildClientProxy(httpClient, routes, mergeStickyOptions(inheritedOptions, { headers }));
30
+ }
31
+ if (!Object.hasOwn(routes, resourceKey)) {
32
+ debug('unknown resource: %s', resourceKey);
33
+ return;
34
+ }
35
+ const resourceRoutes = routes[resourceKey];
36
+ return new Proxy({}, {
37
+ get(_t, methodKey) {
38
+ const route = resourceRoutes[methodKey];
39
+ if (!route) {
40
+ debug('unknown method: %s.%s', resourceKey, methodKey);
41
+ return;
42
+ }
43
+ return (...args) => dispatch(httpClient, route, args, `${resourceKey}.${methodKey}`, inheritedOptions);
44
+ },
45
+ });
46
+ },
47
+ });
48
+ }
@@ -0,0 +1,3 @@
1
+ import type { HerokuApiClient, RequestOptions } from '@heroku/heroku-fetch';
2
+ import type { RouteDefinition } from '@heroku/types/types';
3
+ export declare function dispatch(client: HerokuApiClient, route: RouteDefinition, args: unknown[], invocation?: string, requestOptions?: RequestOptions): Promise<unknown>;
@@ -0,0 +1,80 @@
1
+ import createDebug from 'debug';
2
+ import { countPlaceholders, interpolatePath } from './interpolate-path.js';
3
+ const debug = createDebug('heroku:sdk:dispatcher');
4
+ const MAX_PAGES = 500;
5
+ function hasCallerRangeHeader(requestOptions) {
6
+ if (!requestOptions?.headers)
7
+ return false;
8
+ return Object.keys(requestOptions.headers).some(k => k.toLowerCase() === 'range');
9
+ }
10
+ export async function dispatch(client, route, args, invocation, requestOptions) {
11
+ const placeholderCount = countPlaceholders(route.path);
12
+ const pathParams = args.slice(0, placeholderCount);
13
+ const remaining = args.slice(placeholderCount);
14
+ const path = interpolatePath(route.path, pathParams);
15
+ let body;
16
+ if (route.hasRequestBody && remaining.length > 0) {
17
+ body = remaining[0];
18
+ }
19
+ 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, requestOptions);
21
+ const contentLength = response.headers.get('content-length');
22
+ if (response.status === 204 || contentLength === '0') {
23
+ debug('%s empty response status=%d content-length=%s', invocation ?? 'dispatch', response.status, contentLength ?? '');
24
+ return undefined;
25
+ }
26
+ const result = await response.json();
27
+ if (route.method !== 'GET' || !Array.isArray(result) || hasCallerRangeHeader(requestOptions)) {
28
+ return result;
29
+ }
30
+ const nextRange = response.headers.get('next-range');
31
+ if (!nextRange)
32
+ return result;
33
+ return followPages(client, path, result, nextRange, invocation, requestOptions);
34
+ }
35
+ /* eslint-disable no-await-in-loop -- pagination is inherently sequential; each page depends on the previous next-range header */
36
+ async function followPages(client, path, firstPage, initialRange, invocation, requestOptions) {
37
+ const accumulated = [...firstPage];
38
+ let nextRange = initialRange;
39
+ let pages = 1;
40
+ while (nextRange && pages < MAX_PAGES) {
41
+ const pageOptions = {
42
+ ...requestOptions,
43
+ headers: { ...requestOptions?.headers, Range: nextRange },
44
+ };
45
+ debug('%s paginate page=%d range=%s', invocation ?? 'dispatch', pages + 1, nextRange);
46
+ const pageResponse = await callMethod(client, 'GET', path, undefined, pageOptions);
47
+ const pageBody = await pageResponse.json();
48
+ if (Array.isArray(pageBody)) {
49
+ accumulated.push(...pageBody);
50
+ }
51
+ nextRange = pageResponse.headers.get('next-range');
52
+ pages++;
53
+ }
54
+ debug('%s pagination complete pages=%d total=%d', invocation ?? 'dispatch', pages, accumulated.length);
55
+ return accumulated;
56
+ }
57
+ /* eslint-enable no-await-in-loop */
58
+ function callMethod(client, method, path, body, requestOptions) {
59
+ switch (method) {
60
+ case 'DELETE': {
61
+ return requestOptions ? client.delete(path, requestOptions) : client.delete(path);
62
+ }
63
+ case 'GET': {
64
+ return requestOptions ? client.get(path, requestOptions) : client.get(path);
65
+ }
66
+ case 'PATCH': {
67
+ return requestOptions ? client.patch(path, body, requestOptions) : client.patch(path, body);
68
+ }
69
+ case 'POST': {
70
+ return requestOptions ? client.post(path, body, requestOptions) : client.post(path, body);
71
+ }
72
+ case 'PUT': {
73
+ return requestOptions ? client.put(path, body, requestOptions) : client.put(path, body);
74
+ }
75
+ default: {
76
+ debug('unsupported HTTP method=%s path=%s', method, path);
77
+ throw new Error(`Unsupported HTTP method: ${method}`);
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,24 @@
1
+ import type { DataClient } from '../services/data.js';
2
+ import type { PlatformClient } from '../services/platform.js';
3
+ export type ServiceName = 'data' | 'platform';
4
+ export type ResourceCtx = {
5
+ data: DataClient;
6
+ platform: PlatformClient;
7
+ };
8
+ export type ResourceMethods = Record<string, (...args: any[]) => any>;
9
+ export type ResourceExtension<S extends ServiceName = ServiceName, R extends string = string, M extends ResourceMethods = ResourceMethods> = {
10
+ factory: (ctx: ResourceCtx) => M;
11
+ resource: R;
12
+ service: S;
13
+ };
14
+ export declare function extendResource<S extends ServiceName, R extends string, M extends ResourceMethods>(service: S, resource: R, factory: (ctx: ResourceCtx) => M): ResourceExtension<S, R, M>;
15
+ export type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
16
+ export type ExtensionsFor<Exts extends readonly ResourceExtension[], S extends ServiceName> = Extract<Exts[number], ResourceExtension<S, string, ResourceMethods>>;
17
+ export type MethodsForResource<E, R extends string> = UnionToIntersection<E extends ResourceExtension<ServiceName, R, infer M> ? M : never>;
18
+ type ResourceKeysOf<E> = E extends ResourceExtension<ServiceName, infer R, ResourceMethods> ? R : never;
19
+ export type ApplyExtensions<Base, E> = {
20
+ [K in keyof Base]: K extends ResourceKeysOf<E> ? MethodsForResource<E, K & string> & Omit<Base[K], keyof MethodsForResource<E, K & string>> : Base[K];
21
+ } & {
22
+ [K in ResourceKeysOf<E> as K extends keyof Base ? never : K]: MethodsForResource<E, K & string>;
23
+ };
24
+ export {};
@@ -0,0 +1,3 @@
1
+ export function extendResource(service, resource, factory) {
2
+ return { factory, resource, service };
3
+ }
@@ -0,0 +1,2 @@
1
+ import type { ResourceCtx, ResourceExtension } from './extend-resource.js';
2
+ export declare function mergeExtensions<T extends object>(client: T, extensions: readonly ResourceExtension[], ctx: ResourceCtx): T;
@@ -0,0 +1,56 @@
1
+ import createDebug from 'debug';
2
+ const debug = createDebug('heroku:sdk:extensions');
3
+ export function mergeExtensions(client, extensions, ctx) {
4
+ const methodsByResource = new Map();
5
+ for (const ext of extensions) {
6
+ const factoryMethods = ext.factory(ctx);
7
+ debug('loaded extension service=%s resource=%s methods=%o', ext.service, ext.resource, Object.keys(factoryMethods));
8
+ const merged = methodsByResource.get(ext.resource) ?? {};
9
+ Object.assign(merged, factoryMethods);
10
+ methodsByResource.set(ext.resource, merged);
11
+ }
12
+ for (const [resource, methods] of methodsByResource) {
13
+ const routeResource = client[resource];
14
+ if (routeResource && typeof routeResource === 'object') {
15
+ for (const methodName of Object.keys(methods)) {
16
+ if (methodName in routeResource) {
17
+ debug('extension shadows upstream route: %s.%s', resource, methodName);
18
+ }
19
+ }
20
+ }
21
+ }
22
+ return new Proxy(client, {
23
+ get(target, resourceKey, receiver) {
24
+ // `withOptions` / `withHeaders` on the routes client return a
25
+ // fresh routes proxy without our extensions layer. Intercept
26
+ // them so the result is re-wrapped with the same extensions —
27
+ // otherwise extension methods (like `addOn.listPlans`) silently
28
+ // disappear after a `sdk.platform.withOptions({signal}).addOn`
29
+ // chain.
30
+ if (resourceKey === 'withOptions' || resourceKey === 'withHeaders') {
31
+ const inner = Reflect.get(target, resourceKey, receiver);
32
+ if (typeof inner !== 'function') {
33
+ return inner;
34
+ }
35
+ return (...args) => {
36
+ const rewrapped = inner.apply(target, args);
37
+ return mergeExtensions(rewrapped, extensions, ctx);
38
+ };
39
+ }
40
+ const extMethods = methodsByResource.get(resourceKey);
41
+ const routeResource = Reflect.get(target, resourceKey, receiver);
42
+ if (!extMethods) {
43
+ return routeResource;
44
+ }
45
+ const innerTarget = routeResource && typeof routeResource === 'object' ? routeResource : {};
46
+ return new Proxy(innerTarget, {
47
+ get(routeTarget, methodKey, methodReceiver) {
48
+ if (Object.hasOwn(extMethods, methodKey)) {
49
+ return extMethods[methodKey];
50
+ }
51
+ return Reflect.get(routeTarget, methodKey, methodReceiver);
52
+ },
53
+ });
54
+ },
55
+ });
56
+ }
@@ -0,0 +1,14 @@
1
+ import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
2
+ import type { DataClient } from '../services/data.js';
3
+ import type { PlatformClient } from '../services/platform.js';
4
+ import type { ApplyExtensions, ExtensionsFor, ResourceExtension } from './extend-resource.js';
5
+ export type HerokuSDKOptions<Exts extends readonly ResourceExtension[]> = {
6
+ clientOptions?: HerokuApiClientOptions;
7
+ extensions?: Exts;
8
+ };
9
+ export declare class HerokuSDK<const Exts extends readonly ResourceExtension[] = readonly ResourceExtension[]> {
10
+ #private;
11
+ constructor(options?: HerokuSDKOptions<Exts>);
12
+ get data(): ApplyExtensions<DataClient, ExtensionsFor<Exts, 'data'>>;
13
+ get platform(): ApplyExtensions<PlatformClient, ExtensionsFor<Exts, 'platform'>>;
14
+ }
@@ -0,0 +1,54 @@
1
+ import { createDataClient } from '../services/data.js';
2
+ import { createPlatformClient } from '../services/platform.js';
3
+ import { mergeExtensions } from './extensions-proxy.js';
4
+ function partitionByService(extensions) {
5
+ const map = new Map();
6
+ for (const ext of extensions) {
7
+ const list = map.get(ext.service) ?? [];
8
+ list.push(ext);
9
+ map.set(ext.service, list);
10
+ }
11
+ return map;
12
+ }
13
+ export class HerokuSDK {
14
+ #clientOptions;
15
+ #ctx;
16
+ #data;
17
+ #extensionsByService;
18
+ #platform;
19
+ #rawData;
20
+ #rawPlatform;
21
+ constructor(options = {}) {
22
+ this.#clientOptions = options.clientOptions ?? {};
23
+ this.#extensionsByService = partitionByService(options.extensions ?? []);
24
+ }
25
+ get data() {
26
+ this.#data ??= mergeExtensions(this.#getRawData(), this.#extensionsByService.get('data') ?? [], this.#getCtx());
27
+ return this.#data;
28
+ }
29
+ get platform() {
30
+ this.#platform ??= mergeExtensions(this.#getRawPlatform(), this.#extensionsByService.get('platform') ?? [], this.#getCtx());
31
+ return this.#platform;
32
+ }
33
+ #getCtx() {
34
+ this.#ctx ??= Object.defineProperties({}, {
35
+ data: {
36
+ enumerable: true,
37
+ get: () => this.#getRawData(),
38
+ },
39
+ platform: {
40
+ enumerable: true,
41
+ get: () => this.#getRawPlatform(),
42
+ },
43
+ });
44
+ return this.#ctx;
45
+ }
46
+ #getRawData() {
47
+ this.#rawData ??= createDataClient(this.#clientOptions);
48
+ return this.#rawData;
49
+ }
50
+ #getRawPlatform() {
51
+ this.#rawPlatform ??= createPlatformClient(this.#clientOptions);
52
+ return this.#rawPlatform;
53
+ }
54
+ }
@@ -0,0 +1,3 @@
1
+ export declare const PLACEHOLDER: RegExp;
2
+ export declare function countPlaceholders(template: string): number;
3
+ export declare function interpolatePath(template: string, params: string[]): string;
@@ -0,0 +1,13 @@
1
+ export const PLACEHOLDER = /\{[^}]+\}/g;
2
+ export function countPlaceholders(template) {
3
+ return template.match(PLACEHOLDER)?.length ?? 0;
4
+ }
5
+ export function interpolatePath(template, params) {
6
+ let i = 0;
7
+ return template.replaceAll(PLACEHOLDER, () => {
8
+ if (i >= params.length) {
9
+ throw new Error(`Missing path parameter at index ${i} for template: ${template}`);
10
+ }
11
+ return encodeURIComponent(params[i++]);
12
+ });
13
+ }
@@ -0,0 +1,3 @@
1
+ export { extendResource, type ResourceCtx, type ResourceExtension, type ServiceName, } from './core/extend-resource.js';
2
+ export { HerokuSDK, type HerokuSDKOptions } from './core/heroku-sdk.js';
3
+ export type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { extendResource, } from './core/extend-resource.js';
2
+ export { HerokuSDK } from './core/heroku-sdk.js';
@@ -0,0 +1,9 @@
1
+ import type { BackupCreateResult } from '@heroku/types/data';
2
+ import type { ResourceCtx } from '../../core/extend-resource.js';
3
+ export type BackupOptions = {
4
+ signal?: AbortSignal;
5
+ };
6
+ export declare function create(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: BackupOptions): Promise<BackupCreateResult>;
7
+ export declare const backupExtensions: import("../../index.js").ResourceExtension<"data", "backup", {
8
+ create: (appIdentity: string, addonIdentity?: string, options?: BackupOptions) => Promise<BackupCreateResult>;
9
+ }>;
@@ -0,0 +1,10 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ import { resolvePgDatabase } from './internal/resolve-pg-database.js';
3
+ export async function create(ctx, appIdentity, addonIdentity, options = {}) {
4
+ options.signal?.throwIfAborted();
5
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
6
+ return ctx.data.backup.create(addon.id);
7
+ }
8
+ export const backupExtensions = extendResource('data', 'backup', ctx => ({
9
+ create: (appIdentity, addonIdentity, options) => create(ctx, appIdentity, addonIdentity, options),
10
+ }));
@@ -0,0 +1,54 @@
1
+ import type { DatabaseInfoResult } from '@heroku/types/data';
2
+ import type { ResourceCtx } from '../../core/extend-resource.js';
3
+ export type DatabaseOptions = {
4
+ signal?: AbortSignal;
5
+ };
6
+ export type DatabaseUpgradeBody = {
7
+ version?: string;
8
+ };
9
+ export type DatabaseDescribeResult = DatabaseInfoResult & {
10
+ following?: string;
11
+ 'hot_standby?'?: boolean;
12
+ num_connections?: number;
13
+ num_tables?: number;
14
+ postgres_version?: string;
15
+ 'standalone?'?: boolean;
16
+ };
17
+ export type DatabaseUpgradeWaitResult = {
18
+ 'error?': boolean;
19
+ message: string;
20
+ step: string;
21
+ 'waiting?': boolean;
22
+ };
23
+ export type DatabaseUpgradeResponse = {
24
+ message: string;
25
+ };
26
+ type DatabaseDataClient = {
27
+ database: {
28
+ cancelUpgrade(name: string): Promise<DatabaseUpgradeResponse>;
29
+ dryRunUpgrade(name: string, body: DatabaseUpgradeBody): Promise<DatabaseUpgradeResponse>;
30
+ info(name: string): Promise<DatabaseDescribeResult>;
31
+ prepareUpgrade(name: string, body: DatabaseUpgradeBody): Promise<DatabaseUpgradeResponse>;
32
+ runUpgrade(name: string, body: DatabaseUpgradeBody): Promise<DatabaseUpgradeResponse>;
33
+ upgradeWaitStatus(name: string): Promise<DatabaseUpgradeWaitResult>;
34
+ };
35
+ };
36
+ type DatabaseCtx = {
37
+ data: DatabaseDataClient;
38
+ platform: ResourceCtx['platform'];
39
+ };
40
+ export declare function describe(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, options?: DatabaseOptions): Promise<DatabaseDescribeResult>;
41
+ export declare function upgradeWaitStatus(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, options?: DatabaseOptions): Promise<DatabaseUpgradeWaitResult>;
42
+ export declare function dryRunUpgrade(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, body?: DatabaseUpgradeBody, options?: DatabaseOptions): Promise<DatabaseUpgradeResponse>;
43
+ export declare function runUpgrade(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, body?: DatabaseUpgradeBody, options?: DatabaseOptions): Promise<DatabaseUpgradeResponse>;
44
+ export declare function prepareUpgrade(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, body?: DatabaseUpgradeBody, options?: DatabaseOptions): Promise<DatabaseUpgradeResponse>;
45
+ export declare function cancelUpgrade(ctx: DatabaseCtx, appIdentity: string, addonIdentity?: string, options?: DatabaseOptions): Promise<DatabaseUpgradeResponse>;
46
+ export declare const databaseExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "database", {
47
+ cancelUpgrade: (appIdentity: string, addonIdentity?: string, options?: DatabaseOptions) => Promise<DatabaseUpgradeResponse>;
48
+ describe: (appIdentity: string, addonIdentity?: string, options?: DatabaseOptions) => Promise<DatabaseDescribeResult>;
49
+ dryRunUpgrade: (appIdentity: string, addonIdentity?: string, body?: DatabaseUpgradeBody, options?: DatabaseOptions) => Promise<DatabaseUpgradeResponse>;
50
+ prepareUpgrade: (appIdentity: string, addonIdentity?: string, body?: DatabaseUpgradeBody, options?: DatabaseOptions) => Promise<DatabaseUpgradeResponse>;
51
+ runUpgrade: (appIdentity: string, addonIdentity?: string, body?: DatabaseUpgradeBody, options?: DatabaseOptions) => Promise<DatabaseUpgradeResponse>;
52
+ upgradeWaitStatus: (appIdentity: string, addonIdentity?: string, options?: DatabaseOptions) => Promise<DatabaseUpgradeWaitResult>;
53
+ }>;
54
+ export {};
@@ -0,0 +1,44 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ import { resolvePgDatabase } from './internal/resolve-pg-database.js';
3
+ export async function describe(ctx, appIdentity, addonIdentity, options = {}) {
4
+ options.signal?.throwIfAborted();
5
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
6
+ return ctx.data.database.info(addon.id);
7
+ }
8
+ export async function upgradeWaitStatus(ctx, appIdentity, addonIdentity, options = {}) {
9
+ options.signal?.throwIfAborted();
10
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
11
+ return ctx.data.database.upgradeWaitStatus(addon.id);
12
+ }
13
+ export async function dryRunUpgrade(ctx, appIdentity, addonIdentity, body = {}, options = {}) {
14
+ options.signal?.throwIfAborted();
15
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
16
+ return ctx.data.database.dryRunUpgrade(addon.id, body);
17
+ }
18
+ export async function runUpgrade(ctx, appIdentity, addonIdentity, body = {}, options = {}) {
19
+ options.signal?.throwIfAborted();
20
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
21
+ return ctx.data.database.runUpgrade(addon.id, body);
22
+ }
23
+ export async function prepareUpgrade(ctx, appIdentity, addonIdentity, body = {}, options = {}) {
24
+ options.signal?.throwIfAborted();
25
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
26
+ return ctx.data.database.prepareUpgrade(addon.id, body);
27
+ }
28
+ export async function cancelUpgrade(ctx, appIdentity, addonIdentity, options = {}) {
29
+ options.signal?.throwIfAborted();
30
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
31
+ return ctx.data.database.cancelUpgrade(addon.id);
32
+ }
33
+ export const databaseExtensions = extendResource('data', 'database', rawCtx => {
34
+ // Bridge heroku-types' loose client types to the adapter's strongly-typed contract
35
+ const ctx = rawCtx;
36
+ return {
37
+ cancelUpgrade: (appIdentity, addonIdentity, options) => cancelUpgrade(ctx, appIdentity, addonIdentity, options),
38
+ describe: (appIdentity, addonIdentity, options) => describe(ctx, appIdentity, addonIdentity, options),
39
+ dryRunUpgrade: (appIdentity, addonIdentity, body, options) => dryRunUpgrade(ctx, appIdentity, addonIdentity, body, options),
40
+ prepareUpgrade: (appIdentity, addonIdentity, body, options) => prepareUpgrade(ctx, appIdentity, addonIdentity, body, options),
41
+ runUpgrade: (appIdentity, addonIdentity, body, options) => runUpgrade(ctx, appIdentity, addonIdentity, body, options),
42
+ upgradeWaitStatus: (appIdentity, addonIdentity, options) => upgradeWaitStatus(ctx, appIdentity, addonIdentity, options),
43
+ };
44
+ });
@@ -0,0 +1,2 @@
1
+ import type { PlatformClient } from '../../../services/platform.js';
2
+ export declare function resolveAddonId(platform: PlatformClient, appIdentity: string, addonIdentity?: string): Promise<string>;
@@ -0,0 +1,13 @@
1
+ export async function resolveAddonId(platform, appIdentity, addonIdentity) {
2
+ const matches = await platform.addOnAttachment.resolution({
3
+ // eslint-disable-next-line camelcase
4
+ addon_attachment: addonIdentity ?? 'DATABASE_URL',
5
+ app: appIdentity,
6
+ });
7
+ const attachment = matches[0];
8
+ const addonId = attachment?.addon?.id;
9
+ if (!addonId) {
10
+ throw new Error(`Could not resolve add-on for ${appIdentity}${addonIdentity ? `::${addonIdentity}` : ''}`);
11
+ }
12
+ return addonId;
13
+ }
@@ -0,0 +1,23 @@
1
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
2
+ import type { ResolveAddonOptions, ResolvedAddOn } from '../../platform/add-on/index.js';
3
+ export type ResolvePgDatabaseOptions = ResolveAddonOptions & {
4
+ input?: string;
5
+ };
6
+ /**
7
+ * Resolve a Heroku Postgres database add-on from any of the input
8
+ * shapes the platform recognizes:
9
+ *
10
+ * - omitted → the `DATABASE_URL` attachment on `appIdentity`.
11
+ * - `DATABASE_URL`, `HEROKU_POSTGRESQL_GREEN`, etc. → that attachment
12
+ * on `appIdentity`. Detected by SHOUTY_SNAKE_CASE shape.
13
+ * - `parent-app::branch-name` → branch reference. The portion before
14
+ * `::` is the parent app; the portion after is the branch add-on
15
+ * name.
16
+ * - any other string → a bare add-on identity (UUID, globally-unique
17
+ * name) routed through `resolveAddon`.
18
+ *
19
+ * Throws `AddonNotFoundError` (or `AddonAmbiguousError`) from the
20
+ * underlying resolver. Throws if no input is given and no `appIdentity`
21
+ * is available to default the attachment lookup to.
22
+ */
23
+ export declare function resolvePgDatabase(ctx: Pick<ResourceCtx, 'platform'>, options?: ResolvePgDatabaseOptions): Promise<ResolvedAddOn>;
@@ -0,0 +1,52 @@
1
+ import createDebug from 'debug';
2
+ import { resolveAddon, resolveAddonByAttachment } from '../../platform/add-on/index.js';
3
+ const debug = createDebug('heroku:sdk:resources:database');
4
+ const DEFAULT_PG_ATTACHMENT = 'DATABASE_URL';
5
+ /**
6
+ * Resolve a Heroku Postgres database add-on from any of the input
7
+ * shapes the platform recognizes:
8
+ *
9
+ * - omitted → the `DATABASE_URL` attachment on `appIdentity`.
10
+ * - `DATABASE_URL`, `HEROKU_POSTGRESQL_GREEN`, etc. → that attachment
11
+ * on `appIdentity`. Detected by SHOUTY_SNAKE_CASE shape.
12
+ * - `parent-app::branch-name` → branch reference. The portion before
13
+ * `::` is the parent app; the portion after is the branch add-on
14
+ * name.
15
+ * - any other string → a bare add-on identity (UUID, globally-unique
16
+ * name) routed through `resolveAddon`.
17
+ *
18
+ * Throws `AddonNotFoundError` (or `AddonAmbiguousError`) from the
19
+ * underlying resolver. Throws if no input is given and no `appIdentity`
20
+ * is available to default the attachment lookup to.
21
+ */
22
+ export async function resolvePgDatabase(ctx, options = {}) {
23
+ const { appIdentity, input, ...rest } = options;
24
+ if (!input) {
25
+ if (!appIdentity) {
26
+ throw new Error('resolvePgDatabase requires either input or appIdentity to default to DATABASE_URL.');
27
+ }
28
+ debug('resolve branch=default-attachment app=%s attachment=%s', appIdentity, DEFAULT_PG_ATTACHMENT);
29
+ return resolveAddonByAttachment(ctx, appIdentity, DEFAULT_PG_ATTACHMENT, rest);
30
+ }
31
+ if (input.includes('::')) {
32
+ const { addon, app } = parseBranchReference(input, appIdentity);
33
+ debug('resolve branch=branch-reference input=%s addon=%s app=%s', input, addon, app ?? '<global>');
34
+ return resolveAddon(ctx, addon, { ...rest, appIdentity: app });
35
+ }
36
+ if (appIdentity && isAttachmentName(input)) {
37
+ debug('resolve branch=attachment-name app=%s attachment=%s', appIdentity, input);
38
+ return resolveAddonByAttachment(ctx, appIdentity, input, rest);
39
+ }
40
+ debug('resolve branch=bare-identity input=%s app=%s', input, appIdentity ?? '<global>');
41
+ return resolveAddon(ctx, input, { ...rest, appIdentity });
42
+ }
43
+ function isAttachmentName(input) {
44
+ return /^[A-Z][A-Z0-9_]*$/.test(input);
45
+ }
46
+ function parseBranchReference(reference, fallbackApp) {
47
+ const match = reference.match(/^(.+)::(.+)$/);
48
+ if (match) {
49
+ return { addon: match[2], app: match[1] };
50
+ }
51
+ return fallbackApp ? { addon: reference, app: fallbackApp } : { addon: reference };
52
+ }
@@ -0,0 +1,9 @@
1
+ import type { MaintenanceInfoResult } from '@heroku/types/data';
2
+ import type { ResourceCtx } from '../../core/extend-resource.js';
3
+ export type MaintenanceInfoOptions = {
4
+ signal?: AbortSignal;
5
+ };
6
+ export declare function info(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: MaintenanceInfoOptions): Promise<MaintenanceInfoResult>;
7
+ export declare const maintenanceExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "maintenance", {
8
+ info: (appIdentity: string, addonIdentity?: string, options?: MaintenanceInfoOptions) => Promise<MaintenanceInfoResult>;
9
+ }>;
@@ -0,0 +1,10 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ import { resolvePgDatabase } from './internal/resolve-pg-database.js';
3
+ export async function info(ctx, appIdentity, addonIdentity, options = {}) {
4
+ options.signal?.throwIfAborted();
5
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
6
+ return ctx.data.maintenance.info(addon.id);
7
+ }
8
+ export const maintenanceExtensions = extendResource('data', 'maintenance', ctx => ({
9
+ info: (appIdentity, addonIdentity, options) => info(ctx, appIdentity, addonIdentity, options),
10
+ }));
@@ -0,0 +1,11 @@
1
+ import type { PostgresCredentialInfoResult, PostgresCredentialListResult } from '@heroku/types/data';
2
+ import type { ResourceCtx } from '../../core/extend-resource.js';
3
+ export type PostgresCredentialOptions = {
4
+ signal?: AbortSignal;
5
+ };
6
+ export declare function list(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: PostgresCredentialOptions): Promise<PostgresCredentialListResult>;
7
+ export declare function info(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, credName: string, addonIdentity?: string, options?: PostgresCredentialOptions): Promise<PostgresCredentialInfoResult>;
8
+ export declare const postgresCredentialExtensions: import("../../index.js").ResourceExtension<"data", "postgresCredential", {
9
+ info: (appIdentity: string, credName: string, addonIdentity?: string, options?: PostgresCredentialOptions) => Promise<PostgresCredentialInfoResult>;
10
+ list: (appIdentity: string, addonIdentity?: string, options?: PostgresCredentialOptions) => Promise<PostgresCredentialListResult>;
11
+ }>;
@@ -0,0 +1,16 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ import { resolvePgDatabase } from './internal/resolve-pg-database.js';
3
+ export async function list(ctx, appIdentity, addonIdentity, options = {}) {
4
+ options.signal?.throwIfAborted();
5
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
6
+ return ctx.data.postgresCredential.list(addon.id);
7
+ }
8
+ export async function info(ctx, appIdentity, credName, addonIdentity, options = {}) {
9
+ options.signal?.throwIfAborted();
10
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
11
+ return ctx.data.postgresCredential.info(addon.id, credName);
12
+ }
13
+ export const postgresCredentialExtensions = extendResource('data', 'postgresCredential', ctx => ({
14
+ info: (appIdentity, credName, addonIdentity, options) => info(ctx, appIdentity, credName, addonIdentity, options),
15
+ list: (appIdentity, addonIdentity, options) => list(ctx, appIdentity, addonIdentity, options),
16
+ }));
@@ -0,0 +1,32 @@
1
+ import type { ResourceCtx } from '../../core/extend-resource.js';
2
+ export type ListCredentialsOptions = {
3
+ signal?: AbortSignal;
4
+ };
5
+ export type CredentialInfo = {
6
+ credentials: Array<{
7
+ connections?: null | number;
8
+ password: string;
9
+ state: string;
10
+ user: string;
11
+ }>;
12
+ database: string;
13
+ host: string;
14
+ name: string;
15
+ port: string;
16
+ state: string;
17
+ uuid: string;
18
+ };
19
+ type CredentialsDataClient = {
20
+ postgresDatabase: {
21
+ listCredentials(name: string): Promise<CredentialInfo[]>;
22
+ };
23
+ };
24
+ type CredentialsCtx = {
25
+ data: CredentialsDataClient;
26
+ platform: ResourceCtx['platform'];
27
+ };
28
+ export declare function listCredentials(ctx: CredentialsCtx, appIdentity: string, addonIdentity?: string, options?: ListCredentialsOptions): Promise<CredentialInfo[]>;
29
+ export declare const postgresDatabaseExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "postgresDatabase", {
30
+ listCredentials: (appIdentity: string, addonIdentity?: string, options?: ListCredentialsOptions) => Promise<CredentialInfo[]>;
31
+ }>;
32
+ export {};
@@ -0,0 +1,14 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ import { resolvePgDatabase } from './internal/resolve-pg-database.js';
3
+ export async function listCredentials(ctx, appIdentity, addonIdentity, options = {}) {
4
+ options.signal?.throwIfAborted();
5
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
6
+ return ctx.data.postgresDatabase.listCredentials(addon.id);
7
+ }
8
+ export const postgresDatabaseExtensions = extendResource('data', 'postgresDatabase', rawCtx => {
9
+ // Bridge heroku-types' loose client types to the adapter's strongly-typed contract
10
+ const ctx = rawCtx;
11
+ return {
12
+ listCredentials: (appIdentity, addonIdentity, options) => listCredentials(ctx, appIdentity, addonIdentity, options),
13
+ };
14
+ });