@heroku/sdk 0.11.0 → 0.11.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/dist/resources/data/internal/postgres-utils.d.ts +10 -6
- package/dist/resources/data/internal/postgres-utils.js +11 -7
- package/dist/resources/data/internal/resolve-pg-database.d.ts +24 -21
- package/dist/resources/data/internal/resolve-pg-database.js +151 -85
- package/dist/resources/data/postgres-database.d.ts +1 -0
- package/dist/resources/data/postgres-database.js +1 -0
- package/dist/resources/data/transfer-schedule.js +2 -2
- package/package.json +1 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { AddOn } from '@heroku/types/3.sdk';
|
|
2
|
-
import { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
-
import { AddOnAttachmentWithInclusions } from '../../platform/add-on-attachment/resolve.js';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
import type { AddOnAttachmentWithInclusions } from '../../platform/add-on-attachment/resolve.js';
|
|
4
4
|
/**
|
|
5
|
-
* Returns
|
|
5
|
+
* Returns the first legacy database add-on on the app.
|
|
6
6
|
*/
|
|
7
|
-
export declare function
|
|
8
|
-
export declare const
|
|
9
|
-
|
|
7
|
+
export declare function findLegacyDatabase(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string): Promise<AddOn>;
|
|
8
|
+
export declare const getPostgresServiceName: () => string;
|
|
9
|
+
/**
|
|
10
|
+
* `AddOnAttachmentWithInclusions` is an internal type. The public `isPostgresAddon` takes `AddOn` only.
|
|
11
|
+
*/
|
|
12
|
+
export declare const isPostgresAddonInternal: (addon: AddOn | AddOnAttachmentWithInclusions["addon"]) => boolean;
|
|
13
|
+
export declare const isPostgresAddon: (addon: AddOn) => boolean;
|
|
10
14
|
export declare const isAdvancedDatabase: (addon: AddOn) => boolean;
|
|
11
15
|
export declare const isLegacyDatabase: (addon: AddOn) => boolean;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import createDebug from 'debug';
|
|
2
|
-
const debug = createDebug('postgres-
|
|
2
|
+
const debug = createDebug('heroku:sdk:resources:postgres-database');
|
|
3
3
|
/**
|
|
4
|
-
* Returns
|
|
4
|
+
* Returns the first legacy database add-on on the app.
|
|
5
5
|
*/
|
|
6
|
-
export async function
|
|
6
|
+
export async function findLegacyDatabase(ctx, appIdentity) {
|
|
7
7
|
debug(`fetching arbitrary legacy database on ${appIdentity}`);
|
|
8
8
|
const addons = await ctx.platform.addOn.listByApp(appIdentity);
|
|
9
9
|
const addon = addons.find(a => a.app.name === appIdentity && isLegacyDatabase(a));
|
|
@@ -11,7 +11,11 @@ export async function getArbitraryLegacyDB(ctx, appIdentity) {
|
|
|
11
11
|
throw new Error(`No Heroku Postgres legacy database on ${appIdentity}`);
|
|
12
12
|
return addon;
|
|
13
13
|
}
|
|
14
|
-
export const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export const getPostgresServiceName = () => process.env.HEROKU_POSTGRESQL_ADDON_NAME || process.env.HEROKU_DATA_SERVICE || 'heroku-postgresql';
|
|
15
|
+
/**
|
|
16
|
+
* `AddOnAttachmentWithInclusions` is an internal type. The public `isPostgresAddon` takes `AddOn` only.
|
|
17
|
+
*/
|
|
18
|
+
export const isPostgresAddonInternal = (addon) => addon.plan?.name?.split(':', 2)[0] === getPostgresServiceName();
|
|
19
|
+
export const isPostgresAddon = (addon) => isPostgresAddonInternal(addon);
|
|
20
|
+
export const isAdvancedDatabase = (addon) => isPostgresAddonInternal(addon) && /^(advanced|performance)/.test(addon.plan?.name?.split(':', 2)[1] ?? '');
|
|
21
|
+
export const isLegacyDatabase = (addon) => isPostgresAddonInternal(addon) && !isAdvancedDatabase(addon);
|
|
@@ -10,57 +10,60 @@ export type ResolvePgDatabaseOptions = ResolveAddonAttachmentOptions & {
|
|
|
10
10
|
input?: string;
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
|
-
* Thrown by `resolvePgDatabase` when
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Thrown by `resolvePgDatabase` when a database cannot be resolved: the app
|
|
14
|
+
* has no Postgres databases, the attachment is not found, or the matched
|
|
15
|
+
* attachment is not the configured Postgres service.
|
|
16
16
|
*/
|
|
17
17
|
export declare class PgDatabaseNotFoundError extends Error {
|
|
18
|
-
|
|
19
|
-
cause?: unknown;
|
|
20
|
-
});
|
|
18
|
+
readonly name = "PgDatabaseNotFoundError";
|
|
21
19
|
}
|
|
22
20
|
/**
|
|
23
21
|
* Resolve a Heroku Postgres database add-on from an attachment identity.
|
|
24
22
|
*
|
|
25
23
|
* `input` selects the attachment as follows:
|
|
26
|
-
* - omitted → the `DATABASE_URL` attachment on `appIdentity`.
|
|
24
|
+
* - omitted or `''` → the `DATABASE_URL` attachment on `appIdentity`.
|
|
27
25
|
* - `parent-app::attachment-or-config-var` (e.g. `my-app::DATABASE_URL`)
|
|
28
26
|
* → an attachment or config var on `parent-app`.
|
|
29
27
|
* - `parent-app::branch-name` (e.g. `my-app::my-branch`) → branch add-on
|
|
30
28
|
* name on `parent-app`.
|
|
31
|
-
* - Both `::` forms are handled the same way here: we split on the first
|
|
32
|
-
* `::` ourselves and send `app`/`addon` as separate fields, which
|
|
33
|
-
* works for either shape.
|
|
34
29
|
* - any other string (attachment name, config var, UUID, or
|
|
35
30
|
* globally-unique add-on name) → routed through `resolveAddonAttachment`,
|
|
36
31
|
* scoped to `appIdentity`.
|
|
37
32
|
*
|
|
38
|
-
* This function
|
|
39
|
-
* through the add-on resolve endpoint, call `resolveAddon`
|
|
33
|
+
* This function intentionally relies on the attachment resolve endpoint. To
|
|
34
|
+
* resolve a database through the add-on resolve endpoint, call `resolveAddon`
|
|
35
|
+
* directly.
|
|
40
36
|
*
|
|
41
37
|
* The returned add-on is the identity nested on the attachment (`id`,
|
|
42
38
|
* `name`, `app`, and the plan stub), not a full `ResolvedAddOn`. To get
|
|
43
39
|
* that shape, call `resolveAddon` with the returned add-on's `id`.
|
|
44
40
|
*
|
|
45
|
-
*
|
|
41
|
+
* A single resolver match is returned directly.
|
|
42
|
+
*
|
|
43
|
+
* If the resolver finds multiple matches that all point to the same add-on
|
|
46
44
|
* (same `addon.id`/attachment `app.id`) and whose own `_URL` config var
|
|
47
45
|
* resolves to the same value on the app, they're treated as equivalent
|
|
48
46
|
* (e.g. the same database attached under two names) rather than ambiguous.
|
|
49
47
|
*
|
|
48
|
+
* If the resolver finds no match at all, falls back to treating `input` as
|
|
49
|
+
* a literal config var name (adding a `_URL` suffix if missing) and looking
|
|
50
|
+
* for a Postgres attachment on the app whose own `_URL` config var resolves
|
|
51
|
+
* to the same value. This covers an app config var that is not one of the
|
|
52
|
+
* attachment's own config vars, but whose value is the same database URL.
|
|
53
|
+
*
|
|
50
54
|
* Throws a `PgDatabaseNotFoundError` when the resolve endpoint 404s, the matched
|
|
51
55
|
* attachment is not the configured Postgres service, or the app has no databases.
|
|
52
56
|
* Throws an `AddonAttachmentAmbiguousError` when the remaining matches are not
|
|
53
57
|
* the same database. Throws a plain `Error` if `appIdentity` is empty and `input`
|
|
54
58
|
* has no `::`-prefixed app to fall back on.
|
|
55
59
|
*
|
|
56
|
-
* `resolvePgDatabase` should not throw a bare `NotFoundError`.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* separable by type.
|
|
60
|
+
* `resolvePgDatabase` should not throw a bare `NotFoundError`. Extensions that
|
|
61
|
+
* use it typically couple it with a second data call (e.g. `data.database.info(addon.id)`)
|
|
62
|
+
* that can also 404. Callers often catch `NotFoundError` to handle that second
|
|
63
|
+
* call's failure specifically. However, if a resolution failure surfaced as a
|
|
64
|
+
* `NotFoundError` too, that catch would swallow it and mislabel it as the second
|
|
65
|
+
* call's failure instead. Resolution failures are reported as `PgDatabaseNotFoundError`
|
|
66
|
+
* so the two stay separable by type.
|
|
64
67
|
*/
|
|
65
68
|
export declare function resolvePgDatabase(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options: ResolvePgDatabaseOptions & {
|
|
66
69
|
includeAttachment: true;
|
|
@@ -1,135 +1,201 @@
|
|
|
1
1
|
import { NotFoundError } from '@heroku/heroku-fetch';
|
|
2
2
|
import createDebug from 'debug';
|
|
3
3
|
import { AddonAttachmentAmbiguousError, AddonAttachmentNotFoundError, resolveAddonAttachment } from '../../platform/add-on-attachment/resolve.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getPostgresServiceName, isPostgresAddonInternal } from './postgres-utils.js';
|
|
5
5
|
const debug = createDebug('heroku:sdk:resources:database');
|
|
6
6
|
const DEFAULT_PG_ATTACHMENT = 'DATABASE_URL';
|
|
7
7
|
/**
|
|
8
|
-
* Thrown by `resolvePgDatabase` when
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Thrown by `resolvePgDatabase` when a database cannot be resolved: the app
|
|
9
|
+
* has no Postgres databases, the attachment is not found, or the matched
|
|
10
|
+
* attachment is not the configured Postgres service.
|
|
11
11
|
*/
|
|
12
12
|
export class PgDatabaseNotFoundError extends Error {
|
|
13
|
-
|
|
14
|
-
super(message, options);
|
|
15
|
-
this.name = 'PgDatabaseNotFoundError';
|
|
16
|
-
}
|
|
13
|
+
name = 'PgDatabaseNotFoundError';
|
|
17
14
|
}
|
|
18
15
|
export async function resolvePgDatabase(ctx, appIdentity, options = {}) {
|
|
19
|
-
const { includeAttachment, input, ...rest } = options;
|
|
20
|
-
const databaseReference = input
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
: { addon: databaseReference, app: appIdentity };
|
|
24
|
-
if (!app) {
|
|
16
|
+
const { includeAttachment, input, signal, ...rest } = options;
|
|
17
|
+
const databaseReference = input || DEFAULT_PG_ATTACHMENT;
|
|
18
|
+
const { app: parentApp, attachmentId } = parseDatabaseReference(databaseReference, appIdentity);
|
|
19
|
+
if (!parentApp) {
|
|
25
20
|
throw new Error('resolvePgDatabase requires a non-empty app identity.');
|
|
26
21
|
}
|
|
27
|
-
debug('resolve input=%s
|
|
22
|
+
debug('resolve input=%s attachment=%s app=%s', input ?? '<default>', attachmentId, parentApp);
|
|
28
23
|
let attachment;
|
|
29
24
|
try {
|
|
30
|
-
attachment = await resolveAddonAttachment(ctx,
|
|
25
|
+
attachment = await resolveAddonAttachment(ctx, parentApp, attachmentId, { ...rest, addonService: getPostgresServiceName(), signal });
|
|
31
26
|
}
|
|
32
27
|
catch (error) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
28
|
+
attachment = await attachmentResolutionFallback(ctx, parentApp, attachmentId, error, signal);
|
|
29
|
+
}
|
|
30
|
+
return includeAttachment ? { addon: attachment.addon, attachment } : attachment.addon;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fallback when `resolveAddonAttachment` throws. Returns an attachment when the failure
|
|
34
|
+
* can be recovered from:
|
|
35
|
+
* - `NotFoundError` or `AddonAttachmentNotFoundError` → list the app's Postgres attachments.
|
|
36
|
+
* An empty list throws `PgDatabaseNotFoundError`. Otherwise treat `attachmentId` as a config
|
|
37
|
+
* var (adding `_URL` if missing) and keep every attachment whose own `_URL` value equals it.
|
|
38
|
+
* Those matches already share one `_URL` value by construction, so only a same-add-on check
|
|
39
|
+
* is needed before returning the first one. No matches, or matches that span more than one
|
|
40
|
+
* add-on, throws `PgDatabaseNotFoundError`.
|
|
41
|
+
* - `AddonAttachmentAmbiguousError` → collapse `error.matches` to one attachment via
|
|
42
|
+
* `equivalentAttachment` when they're the same database (same add-on, same attachment app,
|
|
43
|
+
* and the same non-empty `_URL` value fetched from the app's config vars). Otherwise rethrow.
|
|
44
|
+
* - any other error → rethrow.
|
|
45
|
+
*/
|
|
46
|
+
async function attachmentResolutionFallback(ctx, appIdentity, attachmentId, error, signal) {
|
|
47
|
+
if (isAttachmentNotFoundError(error)) {
|
|
48
|
+
const attachments = await listPostgresAttachments(ctx, appIdentity, attachmentId, error, signal);
|
|
49
|
+
if (attachments.length === 0) {
|
|
50
|
+
throw new PgDatabaseNotFoundError(`${appIdentity} has no ${getPostgresServiceName()} databases`, { cause: error });
|
|
54
51
|
}
|
|
55
|
-
|
|
52
|
+
const resolutionError = buildDatabaseNotFoundError(appIdentity, attachmentId, attachments, error);
|
|
53
|
+
const matches = await resolveAttachmentViaConfigVarName(ctx, appIdentity, attachmentId, attachments, resolutionError, signal);
|
|
54
|
+
if (matches.length === 0 || !sameAddon(matches))
|
|
55
|
+
throw resolutionError;
|
|
56
|
+
return matches[0];
|
|
57
|
+
}
|
|
58
|
+
if (error instanceof AddonAttachmentAmbiguousError) {
|
|
59
|
+
const equivalent = await equivalentAttachment(ctx, error.matches, error, signal);
|
|
60
|
+
if (!equivalent)
|
|
56
61
|
throw error;
|
|
57
|
-
|
|
62
|
+
return equivalent;
|
|
58
63
|
}
|
|
59
|
-
|
|
64
|
+
throw error;
|
|
60
65
|
}
|
|
61
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Used by `attachmentResolutionFallback` to collapse ambiguous matches to the first
|
|
68
|
+
* attachment when all matches target the same database: same add-on, same attachment
|
|
69
|
+
* app, and the same non-empty `_URL` value. Returns `undefined` when the matches are
|
|
70
|
+
* not equivalent or the first match has no `_URL` value. `matches` must be non-empty.
|
|
71
|
+
*/
|
|
72
|
+
async function equivalentAttachment(ctx, matches, error, signal) {
|
|
73
|
+
const [first] = matches;
|
|
74
|
+
if (!sameAddon(matches))
|
|
75
|
+
return undefined;
|
|
76
|
+
const appConfig = await listAppConfigVars(ctx, first.app.name, error, signal);
|
|
77
|
+
const firstUrlValue = getUrlConfigVarValue(first, appConfig);
|
|
78
|
+
if (!firstUrlValue)
|
|
79
|
+
return undefined;
|
|
80
|
+
const sameDatabaseUrl = matches.every(match => getUrlConfigVarValue(match, appConfig) === firstUrlValue);
|
|
81
|
+
return sameDatabaseUrl ? first : undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Used by `attachmentResolutionFallback` to list an app's config vars. An aborted
|
|
85
|
+
* signal throws the abort error. Any other listing failure throws `error` (the
|
|
86
|
+
* resolution error for this attempt).
|
|
87
|
+
*/
|
|
88
|
+
async function listAppConfigVars(ctx, appIdentity, error, signal) {
|
|
89
|
+
try {
|
|
90
|
+
return await ctx.platform
|
|
91
|
+
.withOptions({ signal })
|
|
92
|
+
.configVar.infoForApp(appIdentity);
|
|
93
|
+
}
|
|
94
|
+
catch (configVarError) {
|
|
95
|
+
signal?.throwIfAborted();
|
|
96
|
+
debug('config var lookup failed while attempting to resolve via fallback: %O', configVarError);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Used by `attachmentResolutionFallback` to list all attachments on an app that match the
|
|
102
|
+
* add-on service filter. Throws `PgDatabaseNotFoundError` if the app does not exist
|
|
103
|
+
* or the listing fails for any other reason.
|
|
104
|
+
*/
|
|
105
|
+
async function listPostgresAttachments(ctx, appIdentity, attachmentId, error, signal) {
|
|
62
106
|
let attachments;
|
|
63
107
|
try {
|
|
64
|
-
attachments = await
|
|
108
|
+
attachments = await ctx.platform
|
|
109
|
+
.withHeaders({ 'Accept-Inclusion': 'addon:plan,config_vars' })
|
|
110
|
+
.withOptions({ signal })
|
|
111
|
+
.addOnAttachment.listByApp(appIdentity);
|
|
65
112
|
}
|
|
66
113
|
catch (listError) {
|
|
67
114
|
signal?.throwIfAborted();
|
|
68
115
|
if (listError instanceof NotFoundError) {
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// the app doesn't exist, so we rethrow the error to cover that case.
|
|
116
|
+
// Listing attachments 404s only when the app does not exist, which would
|
|
117
|
+
// also be the reason for the attachment resolution failure. Therefore,
|
|
118
|
+
// we surface this error specifically.
|
|
73
119
|
throw new PgDatabaseNotFoundError(`Couldn't find that app: ${appIdentity}`, { cause: listError });
|
|
74
120
|
}
|
|
75
121
|
debug('listing attachments failed while building not-found error: %O', listError);
|
|
76
122
|
throw new PgDatabaseNotFoundError(`Could not resolve ${stripUrlSuffix(attachmentId)} on ${appIdentity}`, { cause: error });
|
|
77
123
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
124
|
+
return attachments.filter(attachment => isPostgresAddonInternal(attachment.addon));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Used by `attachmentResolutionFallback` to resolve attachments via app config vars
|
|
128
|
+
* when no matches were found. Treats `attachmentId` as a config var name and
|
|
129
|
+
* uses its value to filter the attachments. Returns an empty array if the
|
|
130
|
+
* config var value is not found.
|
|
131
|
+
*/
|
|
132
|
+
async function resolveAttachmentViaConfigVarName(ctx, appIdentity, attachmentId, attachments, error, signal) {
|
|
133
|
+
const appConfig = await listAppConfigVars(ctx, appIdentity, error, signal);
|
|
134
|
+
const inputConfigVarName = attachmentId.endsWith('_URL') ? attachmentId : `${attachmentId}_URL`;
|
|
135
|
+
const inputConfigVarValue = appConfig[inputConfigVarName];
|
|
136
|
+
if (!inputConfigVarValue)
|
|
137
|
+
return [];
|
|
138
|
+
return attachments.filter(attachment => getUrlConfigVarValue(attachment, appConfig) === inputConfigVarValue);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Builds a `PgDatabaseNotFoundError` when attachment resolution endpoint 404s (NotFoundError)
|
|
142
|
+
* or attachments did not match the add-on service filter (AddonAttachmentNotFoundError).
|
|
143
|
+
*/
|
|
144
|
+
function buildDatabaseNotFoundError(appIdentity, attachmentId, attachments, error) {
|
|
81
145
|
const validOptions = attachments
|
|
82
146
|
.map(attachment => {
|
|
83
|
-
const urlVarName =
|
|
147
|
+
const urlVarName = getUrlConfigVarName(attachment.config_vars);
|
|
84
148
|
return urlVarName ? stripUrlSuffix(urlVarName) : attachment.name;
|
|
85
149
|
})
|
|
86
150
|
.join(', ');
|
|
87
151
|
const databaseName = stripUrlSuffix(attachmentId);
|
|
88
152
|
if (error instanceof AddonAttachmentNotFoundError) {
|
|
89
|
-
return new PgDatabaseNotFoundError(`${databaseName} is not a ${
|
|
153
|
+
return new PgDatabaseNotFoundError(`${databaseName} is not a ${getPostgresServiceName()} database on ${appIdentity}. Valid options are: ${validOptions}`, { cause: error });
|
|
90
154
|
}
|
|
91
155
|
return new PgDatabaseNotFoundError(`Unknown database: ${databaseName} on ${appIdentity}. Valid options are: ${validOptions}`, { cause: error });
|
|
92
156
|
}
|
|
93
|
-
async function listPostgresAttachments(ctx, appIdentity, signal) {
|
|
94
|
-
const attachments = await ctx.platform
|
|
95
|
-
.withHeaders({ 'Accept-Inclusion': 'addon:plan,config_vars' })
|
|
96
|
-
.withOptions({ signal })
|
|
97
|
-
.addOnAttachment.listByApp(appIdentity);
|
|
98
|
-
return attachments.filter(attachment => isPostgresAddon(attachment.addon));
|
|
99
|
-
}
|
|
100
|
-
function stripUrlSuffix(name) {
|
|
101
|
-
return name.endsWith('_URL') ? name.slice(0, -4) : name;
|
|
102
|
-
}
|
|
103
157
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* trip) unless every match shares the same add-on and attachment app.
|
|
158
|
+
* Returns the first config var name declared on an attachment that holds its connection
|
|
159
|
+
* URL or `undefined` if no such config var is found.
|
|
107
160
|
*/
|
|
108
|
-
function
|
|
109
|
-
const [first] = matches;
|
|
110
|
-
const sameAddon = matches.every(match => match.addon.id === first.addon.id && match.app.id === first.app.id);
|
|
111
|
-
return sameAddon ? matches : undefined;
|
|
112
|
-
}
|
|
113
|
-
/** The first config var name declared on an attachment that holds its connection URL. */
|
|
114
|
-
function urlConfigVarName(configVars) {
|
|
161
|
+
function getUrlConfigVarName(configVars) {
|
|
115
162
|
return configVars.find(name => name.endsWith('_URL'));
|
|
116
163
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return matches.every(match => {
|
|
125
|
-
const varName = urlConfigVarName(match.config_vars);
|
|
126
|
-
return varName !== undefined && config[varName] === firstValue;
|
|
127
|
-
});
|
|
164
|
+
/**
|
|
165
|
+
* Looks up the value of `attachment`'s own `_URL` config var in `appConfig`, or
|
|
166
|
+
* `undefined` if the attachment has no `_URL`-suffixed config var.
|
|
167
|
+
*/
|
|
168
|
+
function getUrlConfigVarValue(attachment, appConfig) {
|
|
169
|
+
const urlVarName = getUrlConfigVarName(attachment.config_vars);
|
|
170
|
+
return urlVarName ? appConfig[urlVarName] : undefined;
|
|
128
171
|
}
|
|
129
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Returns true if `error` is a NotFoundError (attachment resolution endpoint 404) or
|
|
174
|
+
* AddonAttachmentNotFoundError (attachments did not match the add-on service filter).
|
|
175
|
+
*/
|
|
176
|
+
function isAttachmentNotFoundError(error) {
|
|
177
|
+
return error instanceof AddonAttachmentNotFoundError || error instanceof NotFoundError;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Splits an `app::database` reference into the app and the attachment id.
|
|
181
|
+
* A reference with no `::` uses `appIdentity` as the app and the whole
|
|
182
|
+
* reference as the attachment id.
|
|
183
|
+
*/
|
|
184
|
+
function parseDatabaseReference(reference, appIdentity) {
|
|
130
185
|
const match = reference.match(/^(.+?)::(.+)$/);
|
|
131
186
|
if (match) {
|
|
132
|
-
return {
|
|
187
|
+
return { app: match[1], attachmentId: match[2] };
|
|
133
188
|
}
|
|
134
|
-
return {
|
|
189
|
+
return { app: appIdentity, attachmentId: reference };
|
|
190
|
+
}
|
|
191
|
+
/** Whether every match shares the same add-on and attachment app. */
|
|
192
|
+
function sameAddon(matches) {
|
|
193
|
+
const [first] = matches;
|
|
194
|
+
return matches.every(match => match.addon.id === first.addon.id && match.app.id === first.app.id);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Removes the `_URL` suffix from a config var name if it exists.
|
|
198
|
+
*/
|
|
199
|
+
function stripUrlSuffix(name) {
|
|
200
|
+
return name.endsWith('_URL') ? name.slice(0, -4) : name;
|
|
135
201
|
}
|
|
@@ -20,3 +20,4 @@ export declare function listCredentials(ctx: Pick<ResourceCtx, 'data' | 'platfor
|
|
|
20
20
|
export declare const postgresDatabaseExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "postgresDatabase", {
|
|
21
21
|
listCredentials: (appIdentity: string, addonIdentity?: string, options?: ListCredentialsOptions) => Promise<CredentialInfo[]>;
|
|
22
22
|
}>;
|
|
23
|
+
export { findLegacyDatabase, getPostgresServiceName, isAdvancedDatabase, isLegacyDatabase, isPostgresAddon, } from './internal/postgres-utils.js';
|
|
@@ -8,3 +8,4 @@ export async function listCredentials(ctx, appIdentity, addonIdentity, options =
|
|
|
8
8
|
export const postgresDatabaseExtensions = extendResource('data', 'postgresDatabase', ctx => ({
|
|
9
9
|
listCredentials: (appIdentity, addonIdentity, options) => listCredentials(ctx, appIdentity, addonIdentity, options),
|
|
10
10
|
}));
|
|
11
|
+
export { findLegacyDatabase, getPostgresServiceName, isAdvancedDatabase, isLegacyDatabase, isPostgresAddon, } from './internal/postgres-utils.js';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { extendResource } from '../../core/extend-resource.js';
|
|
2
|
-
import {
|
|
2
|
+
import { findLegacyDatabase } from './internal/postgres-utils.js';
|
|
3
3
|
import { resolvePgDatabase } from './internal/resolve-pg-database.js';
|
|
4
4
|
export async function list(ctx, appIdentity, options = {}) {
|
|
5
5
|
options.signal?.throwIfAborted();
|
|
6
6
|
// Schedules are app-scoped, so any legacy DB ID unlocks the app's list.
|
|
7
|
-
const addon = await
|
|
7
|
+
const addon = await findLegacyDatabase(ctx, appIdentity);
|
|
8
8
|
return ctx.data.transferSchedule.list(addon.id);
|
|
9
9
|
}
|
|
10
10
|
export async function create(ctx, appIdentity, addonIdentity, body, options = {}) {
|