Skip to content

Commit dceb196

Browse files
authoredJan 15, 2025··
feat: pull resource names for provisioning from config if provided (#7733)
* check flag!! * respect bucket_name, database_name and jurisdiction * changeset * check if bucket/db exists * generic matcher * inherit if no name is provided * more tests * fixups * fixup!
1 parent 97603f0 commit dceb196

File tree

5 files changed

+565
-100
lines changed

5 files changed

+565
-100
lines changed
 

‎.changeset/smart-lions-suffer.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
feat: pull resource names for provisioning from config if provided
6+
7+
Uses `database_name` and `bucket_name` for provisioning if specified. For R2, this only happens if there is not a bucket with that name already. Also respects R2 `jurisdiction` if provided.

‎packages/wrangler/src/__tests__/provision.test.ts

+383-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { http, HttpResponse } from "msw";
22
import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
33
import { mockConsoleMethods } from "./helpers/mock-console";
4-
import { clearDialogs, mockPrompt, mockSelect } from "./helpers/mock-dialogs";
4+
import {
5+
clearDialogs,
6+
mockConfirm,
7+
mockPrompt,
8+
mockSelect,
9+
} from "./helpers/mock-dialogs";
510
import { useMockIsTTY } from "./helpers/mock-istty";
611
import {
712
mockCreateKVNamespace,
@@ -19,6 +24,7 @@ import { runInTempDir } from "./helpers/run-in-tmp";
1924
import { runWrangler } from "./helpers/run-wrangler";
2025
import { writeWorkerSource } from "./helpers/write-worker-source";
2126
import { writeWranglerConfig } from "./helpers/write-wrangler-config";
27+
import type { DatabaseInfo } from "../d1/types";
2228
import type { Settings } from "../deployment-bundle/bindings";
2329

2430
describe("--x-provision", () => {
@@ -86,7 +92,7 @@ describe("--x-provision", () => {
8692
],
8793
});
8894

89-
await expect(runWrangler("deploy --x-provision")).resolves.toBeUndefined();
95+
await runWrangler("deploy --x-provision");
9096
expect(std.out).toMatchInlineSnapshot(`
9197
"Total Upload: xx KiB / gzip: xx KiB
9298
Worker Startup Time: 100 ms
@@ -479,6 +485,337 @@ describe("--x-provision", () => {
479485
expect(std.err).toMatchInlineSnapshot(`""`);
480486
expect(std.warn).toMatchInlineSnapshot(`""`);
481487
});
488+
489+
it("can prefill d1 database name from config file if provided", async () => {
490+
writeWranglerConfig({
491+
main: "index.js",
492+
d1_databases: [{ binding: "D1", database_name: "prefilled-d1-name" }],
493+
});
494+
mockGetSettings();
495+
msw.use(
496+
http.get("*/accounts/:accountId/d1/database", async () => {
497+
return HttpResponse.json(
498+
createFetchResult([
499+
{
500+
name: "db-name",
501+
uuid: "existing-d1-id",
502+
},
503+
])
504+
);
505+
})
506+
);
507+
508+
// no name prompt
509+
mockCreateD1Database({
510+
assertName: "prefilled-d1-name",
511+
resultId: "new-d1-id",
512+
});
513+
514+
mockConfirm({
515+
text: `Would you like to create a new D1 Database named "prefilled-d1-name"?`,
516+
result: true,
517+
});
518+
mockUploadWorkerRequest({
519+
expectedBindings: [
520+
{
521+
name: "D1",
522+
type: "d1",
523+
id: "new-d1-id",
524+
},
525+
],
526+
});
527+
528+
await runWrangler("deploy --x-provision");
529+
530+
expect(std.out).toMatchInlineSnapshot(`
531+
"Total Upload: xx KiB / gzip: xx KiB
532+
533+
The following bindings need to be provisioned:
534+
- D1 Databases:
535+
- D1
536+
537+
Provisioning D1 (D1 Database)...
538+
Resource name found in config: prefilled-d1-name
539+
No pre-existing resource found with that name
540+
🌀 Creating new D1 Database \\"prefilled-d1-name\\"...
541+
✨ D1 provisioned with prefilled-d1-name
542+
543+
--------------------------------------
544+
545+
🎉 All resources provisioned, continuing with deployment...
546+
547+
Worker Startup Time: 100 ms
548+
Your worker has access to the following bindings:
549+
- D1 Databases:
550+
- D1: prefilled-d1-name (new-d1-id)
551+
Uploaded test-name (TIMINGS)
552+
Deployed test-name triggers (TIMINGS)
553+
https://test-name.test-sub-domain.workers.dev
554+
Current Version ID: Galaxy-Class"
555+
`);
556+
expect(std.err).toMatchInlineSnapshot(`""`);
557+
expect(std.warn).toMatchInlineSnapshot(`""`);
558+
});
559+
560+
it("can inherit d1 binding when the database name is provided", async () => {
561+
writeWranglerConfig({
562+
main: "index.js",
563+
d1_databases: [{ binding: "D1", database_name: "prefilled-d1-name" }],
564+
});
565+
mockGetSettings({
566+
result: {
567+
bindings: [
568+
{
569+
type: "d1",
570+
name: "D1",
571+
id: "d1-id",
572+
},
573+
],
574+
},
575+
});
576+
mockGetD1Database("d1-id", { name: "prefilled-d1-name" });
577+
mockUploadWorkerRequest({
578+
expectedBindings: [
579+
{
580+
name: "D1",
581+
type: "inherit",
582+
},
583+
],
584+
});
585+
586+
await runWrangler("deploy --x-provision");
587+
expect(std.out).toMatchInlineSnapshot(`
588+
"Total Upload: xx KiB / gzip: xx KiB
589+
Worker Startup Time: 100 ms
590+
Your worker has access to the following bindings:
591+
- D1 Databases:
592+
- D1: prefilled-d1-name
593+
Uploaded test-name (TIMINGS)
594+
Deployed test-name triggers (TIMINGS)
595+
https://test-name.test-sub-domain.workers.dev
596+
Current Version ID: Galaxy-Class"
597+
`);
598+
});
599+
600+
it("will not inherit d1 binding when the database name is provided but has changed", async () => {
601+
// first deploy used old-d1-name/old-d1-id
602+
// now we provide a different database_name that doesn't match
603+
writeWranglerConfig({
604+
main: "index.js",
605+
d1_databases: [{ binding: "D1", database_name: "new-d1-name" }],
606+
});
607+
mockGetSettings({
608+
result: {
609+
bindings: [
610+
{
611+
type: "d1",
612+
name: "D1",
613+
id: "old-d1-id",
614+
},
615+
],
616+
},
617+
});
618+
msw.use(
619+
http.get("*/accounts/:accountId/d1/database", async () => {
620+
return HttpResponse.json(
621+
createFetchResult([
622+
{
623+
name: "old-d1-name",
624+
uuid: "old-d1-id",
625+
},
626+
])
627+
);
628+
})
629+
);
630+
631+
mockGetD1Database("old-d1-id", { name: "old-d1-name" });
632+
633+
// no name prompt
634+
mockCreateD1Database({
635+
assertName: "new-d1-name",
636+
resultId: "new-d1-id",
637+
});
638+
639+
mockConfirm({
640+
text: `Would you like to create a new D1 Database named "new-d1-name"?`,
641+
result: true,
642+
});
643+
mockUploadWorkerRequest({
644+
expectedBindings: [
645+
{
646+
name: "D1",
647+
type: "d1",
648+
id: "new-d1-id",
649+
},
650+
],
651+
});
652+
653+
await runWrangler("deploy --x-provision");
654+
655+
expect(std.out).toMatchInlineSnapshot(`
656+
"Total Upload: xx KiB / gzip: xx KiB
657+
658+
The following bindings need to be provisioned:
659+
- D1 Databases:
660+
- D1
661+
662+
Provisioning D1 (D1 Database)...
663+
Resource name found in config: new-d1-name
664+
No pre-existing resource found with that name
665+
🌀 Creating new D1 Database \\"new-d1-name\\"...
666+
✨ D1 provisioned with new-d1-name
667+
668+
--------------------------------------
669+
670+
🎉 All resources provisioned, continuing with deployment...
671+
672+
Worker Startup Time: 100 ms
673+
Your worker has access to the following bindings:
674+
- D1 Databases:
675+
- D1: new-d1-name (new-d1-id)
676+
Uploaded test-name (TIMINGS)
677+
Deployed test-name triggers (TIMINGS)
678+
https://test-name.test-sub-domain.workers.dev
679+
Current Version ID: Galaxy-Class"
680+
`);
681+
expect(std.err).toMatchInlineSnapshot(`""`);
682+
expect(std.warn).toMatchInlineSnapshot(`""`);
683+
});
684+
685+
it("can prefill r2 bucket name from config file if provided", async () => {
686+
writeWranglerConfig({
687+
main: "index.js",
688+
r2_buckets: [
689+
{
690+
binding: "BUCKET",
691+
bucket_name: "prefilled-r2-name",
692+
// note it will also respect jurisdiction if provided, but wont prompt for it
693+
jurisdiction: "eu",
694+
},
695+
],
696+
});
697+
mockGetSettings();
698+
msw.use(
699+
http.get("*/accounts/:accountId/r2/buckets", async () => {
700+
return HttpResponse.json(
701+
createFetchResult({
702+
buckets: [
703+
{
704+
name: "existing-bucket-name",
705+
},
706+
],
707+
})
708+
);
709+
})
710+
);
711+
mockGetR2Bucket("prefilled-r2-name", true);
712+
// no name prompt
713+
mockCreateR2Bucket({
714+
assertBucketName: "prefilled-r2-name",
715+
assertJurisdiction: "eu",
716+
});
717+
718+
mockConfirm({
719+
text: `Would you like to create a new R2 Bucket named "prefilled-r2-name"?`,
720+
result: true,
721+
});
722+
mockUploadWorkerRequest({
723+
expectedBindings: [
724+
{
725+
name: "BUCKET",
726+
type: "r2_bucket",
727+
bucket_name: "prefilled-r2-name",
728+
jurisdiction: "eu",
729+
},
730+
],
731+
});
732+
733+
await runWrangler("deploy --x-provision");
734+
735+
expect(std.out).toMatchInlineSnapshot(`
736+
"Total Upload: xx KiB / gzip: xx KiB
737+
738+
The following bindings need to be provisioned:
739+
- R2 Buckets:
740+
- BUCKET
741+
742+
Provisioning BUCKET (R2 Bucket)...
743+
Resource name found in config: prefilled-r2-name
744+
No pre-existing resource found with that name
745+
🌀 Creating new R2 Bucket \\"prefilled-r2-name\\"...
746+
✨ BUCKET provisioned with prefilled-r2-name
747+
748+
--------------------------------------
749+
750+
🎉 All resources provisioned, continuing with deployment...
751+
752+
Worker Startup Time: 100 ms
753+
Your worker has access to the following bindings:
754+
- R2 Buckets:
755+
- BUCKET: prefilled-r2-name (eu)
756+
Uploaded test-name (TIMINGS)
757+
Deployed test-name triggers (TIMINGS)
758+
https://test-name.test-sub-domain.workers.dev
759+
Current Version ID: Galaxy-Class"
760+
`);
761+
expect(std.err).toMatchInlineSnapshot(`""`);
762+
expect(std.warn).toMatchInlineSnapshot(`""`);
763+
});
764+
765+
// to maintain current behaviour
766+
it("wont prompt to provision if an r2 bucket name belongs to an existing bucket", async () => {
767+
writeWranglerConfig({
768+
main: "index.js",
769+
r2_buckets: [
770+
{
771+
binding: "BUCKET",
772+
bucket_name: "existing-bucket-name",
773+
jurisdiction: "eu",
774+
},
775+
],
776+
});
777+
mockGetSettings();
778+
msw.use(
779+
http.get("*/accounts/:accountId/r2/buckets", async () => {
780+
return HttpResponse.json(
781+
createFetchResult({
782+
buckets: [
783+
{
784+
name: "existing-bucket-name",
785+
},
786+
],
787+
})
788+
);
789+
})
790+
);
791+
mockGetR2Bucket("existing-bucket-name", false);
792+
mockUploadWorkerRequest({
793+
expectedBindings: [
794+
{
795+
name: "BUCKET",
796+
type: "r2_bucket",
797+
bucket_name: "existing-bucket-name",
798+
jurisdiction: "eu",
799+
},
800+
],
801+
});
802+
803+
await runWrangler("deploy --x-provision");
804+
805+
expect(std.out).toMatchInlineSnapshot(`
806+
"Total Upload: xx KiB / gzip: xx KiB
807+
Worker Startup Time: 100 ms
808+
Your worker has access to the following bindings:
809+
- R2 Buckets:
810+
- BUCKET: existing-bucket-name (eu)
811+
Uploaded test-name (TIMINGS)
812+
Deployed test-name triggers (TIMINGS)
813+
https://test-name.test-sub-domain.workers.dev
814+
Current Version ID: Galaxy-Class"
815+
`);
816+
expect(std.err).toMatchInlineSnapshot(`""`);
817+
expect(std.warn).toMatchInlineSnapshot(`""`);
818+
});
482819
});
483820

484821
it("should error if used with a service environment", async () => {
@@ -555,6 +892,7 @@ function mockCreateD1Database(
555892
function mockCreateR2Bucket(
556893
options: {
557894
assertBucketName?: string;
895+
assertJurisdiction?: string;
558896
} = {}
559897
) {
560898
msw.use(
@@ -563,11 +901,53 @@ function mockCreateR2Bucket(
563901
async ({ request }) => {
564902
if (options.assertBucketName) {
565903
const requestBody = await request.json();
566-
expect(requestBody).toEqual({ name: options.assertBucketName });
904+
expect(requestBody).toMatchObject({ name: options.assertBucketName });
905+
}
906+
if (options.assertJurisdiction) {
907+
expect(request.headers.get("cf-r2-jurisdiction")).toEqual(
908+
options.assertJurisdiction
909+
);
910+
}
911+
return HttpResponse.json(createFetchResult({}));
912+
},
913+
{ once: true }
914+
)
915+
);
916+
}
917+
918+
function mockGetR2Bucket(bucketName: string, missing: boolean = false) {
919+
msw.use(
920+
http.get(
921+
"*/accounts/:accountId/r2/buckets/:bucketName",
922+
async ({ params }) => {
923+
const { bucketName: bucketParam } = params;
924+
expect(bucketParam).toEqual(bucketName);
925+
if (missing) {
926+
return HttpResponse.json(
927+
createFetchResult(null, false, [
928+
{ code: 10006, message: "bucket not found" },
929+
])
930+
);
567931
}
568932
return HttpResponse.json(createFetchResult({}));
569933
},
570934
{ once: true }
571935
)
572936
);
573937
}
938+
939+
function mockGetD1Database(
940+
databaseId: string,
941+
databaseInfo: Partial<DatabaseInfo>
942+
) {
943+
msw.use(
944+
http.get(
945+
`*/accounts/:accountId/d1/database/:database_id`,
946+
({ params }) => {
947+
expect(params.database_id).toEqual(databaseId);
948+
return HttpResponse.json(createFetchResult(databaseInfo));
949+
},
950+
{ once: true }
951+
)
952+
);
953+
}

‎packages/wrangler/src/d1/list.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const listDatabases = async (
5656
);
5757
page++;
5858
results.push(...json);
59-
if (limitCalls) {
59+
if (limitCalls && page > 3) {
6060
break;
6161
}
6262
if (json.length < pageSize) {

‎packages/wrangler/src/deploy/deploy.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { loadSourceMaps } from "../deployment-bundle/source-maps";
2828
import { confirm } from "../dialogs";
2929
import { getMigrationsToUpload } from "../durable";
3030
import { UserError } from "../errors";
31+
import { getFlag } from "../experimental-flags";
3132
import { logger } from "../logger";
3233
import { getMetricsUsageHeaders } from "../metrics";
3334
import { isNavigatorDefined } from "../navigator-user-agent";
@@ -799,13 +800,15 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
799800
} else {
800801
assert(accountId, "Missing accountId");
801802

802-
await provisionBindings(
803-
bindings,
804-
accountId,
805-
scriptName,
806-
props.experimentalAutoCreate,
807-
props.config
808-
);
803+
getFlag("RESOURCES_PROVISION")
804+
? await provisionBindings(
805+
bindings,
806+
accountId,
807+
scriptName,
808+
props.experimentalAutoCreate,
809+
props.config
810+
)
811+
: null;
809812
await ensureQueuesExistByConfig(config);
810813
let bindingsPrinted = false;
811814

‎packages/wrangler/src/deployment-bundle/bindings.ts

+164-89
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import assert from "node:assert";
12
import chalk from "chalk";
23
import { fetchResult } from "../cfetch";
34
import { createD1Database } from "../d1/create";
45
import { listDatabases } from "../d1/list";
5-
import { prompt, select } from "../dialogs";
6-
import { FatalError, UserError } from "../errors";
6+
import { getDatabaseInfoFromId } from "../d1/utils";
7+
import { confirm, prompt, select } from "../dialogs";
8+
import { UserError } from "../errors";
79
import { createKVNamespace, listKVNamespaces } from "../kv/helpers";
810
import { logger } from "../logger";
9-
import { createR2Bucket, listR2Buckets } from "../r2/helpers";
11+
import { APIError } from "../parse";
12+
import { createR2Bucket, getR2Bucket, listR2Buckets } from "../r2/helpers";
1013
import { isLegacyEnv } from "../utils/isLegacyEnv";
1114
import { printBindings } from "../utils/print-bindings";
1215
import type { Config } from "../config";
@@ -76,6 +79,8 @@ export type Settings = {
7679
};
7780

7881
type PendingResourceOperations = {
82+
// name may be provided in config without the resource having been provisioned
83+
name?: string | undefined;
7984
create: (name: string) => Promise<string>;
8085
updateId: (id: string) => void;
8186
};
@@ -113,7 +118,6 @@ export async function provisionBindings(
113118
binding: kv.binding,
114119
async create(title) {
115120
const id = await createKVNamespace(accountId, title);
116-
kv.id = id;
117121
return id;
118122
},
119123
updateId(id) {
@@ -125,42 +129,78 @@ export async function provisionBindings(
125129
}
126130

127131
for (const r2 of bindings.r2_buckets ?? []) {
128-
if (!r2.bucket_name) {
129-
if (inBindingSettings(settings, "r2_bucket", r2.binding)) {
130-
r2.bucket_name = INHERIT_SYMBOL;
131-
} else {
132-
pendingResources.r2_buckets?.push({
133-
binding: r2.binding,
134-
async create(bucketName) {
135-
await createR2Bucket(accountId, bucketName);
136-
r2.bucket_name = bucketName;
137-
return bucketName;
138-
},
139-
updateId(bucketName) {
140-
r2.bucket_name = bucketName;
141-
},
142-
});
132+
assert(typeof r2.bucket_name !== "symbol");
133+
if (
134+
inBindingSettings(settings, "r2_bucket", r2.binding, {
135+
bucket_name: r2.bucket_name,
136+
})
137+
) {
138+
// does not inherit if the bucket name has changed
139+
r2.bucket_name = INHERIT_SYMBOL;
140+
} else {
141+
if (r2.bucket_name) {
142+
try {
143+
await getR2Bucket(accountId, r2.bucket_name);
144+
// don't provision
145+
continue;
146+
} catch (e) {
147+
// bucket not found - provision
148+
if (!(e instanceof APIError && e.code === 10006)) {
149+
throw e;
150+
}
151+
}
143152
}
153+
pendingResources.r2_buckets?.push({
154+
binding: r2.binding,
155+
name: r2.bucket_name,
156+
async create(bucketName) {
157+
await createR2Bucket(
158+
accountId,
159+
bucketName,
160+
undefined,
161+
// respect jurisdiction if it has been specified in the config, but don't prompt
162+
r2.jurisdiction
163+
);
164+
return bucketName;
165+
},
166+
updateId(bucketName) {
167+
r2.bucket_name = bucketName;
168+
},
169+
});
144170
}
145171
}
146172

147173
for (const d1 of bindings.d1_databases ?? []) {
148174
if (!d1.database_id) {
149-
if (inBindingSettings(settings, "d1", d1.binding)) {
150-
d1.database_id = INHERIT_SYMBOL;
151-
} else {
152-
pendingResources.d1_databases?.push({
153-
binding: d1.binding,
154-
async create(name) {
155-
const db = await createD1Database(accountId, name);
156-
d1.database_id = db.uuid;
157-
return db.uuid;
158-
},
159-
updateId(id) {
160-
d1.database_id = id;
161-
},
162-
});
175+
const maybeInherited = inBindingSettings(settings, "d1", d1.binding);
176+
if (maybeInherited) {
177+
if (!d1.database_name) {
178+
d1.database_id = INHERIT_SYMBOL;
179+
continue;
180+
} else {
181+
// check that the database name matches the id of the inherited binding
182+
const dbFromId = await getDatabaseInfoFromId(
183+
accountId,
184+
maybeInherited.id
185+
);
186+
if (d1.database_name === dbFromId.name) {
187+
d1.database_id = INHERIT_SYMBOL;
188+
continue;
189+
}
190+
}
191+
// otherwise, db name has *changed* - re-provision
163192
}
193+
pendingResources.d1_databases?.push({
194+
binding: d1.binding,
195+
name: d1.database_name,
196+
async create(name) {
197+
const db = await createD1Database(accountId, name);
198+
return db.uuid;
199+
},
200+
updateId(id) {
201+
d1.database_id = id;
202+
},
203+
});
164204
}
165205
}
166206

@@ -216,14 +256,24 @@ export async function provisionBindings(
216256
}
217257

218258
/** checks whether the binding id can be inherited from a prev deployment */
259+
type ExtractedBinding<T> = Extract<WorkerMetadataBinding, { type: T }>;
219260
function inBindingSettings<Type extends WorkerMetadataBinding["type"]>(
220261
settings: Settings | undefined,
221262
type: Type,
222-
bindingName: string
223-
): Extract<WorkerMetadataBinding, { type: Type }> | undefined {
263+
bindingName: string,
264+
other?: Partial<Record<keyof ExtractedBinding<Type>, unknown>>
265+
): ExtractedBinding<Type> | undefined {
224266
return settings?.bindings.find(
225-
(binding): binding is Extract<WorkerMetadataBinding, { type: Type }> =>
226-
binding.type === type && binding.name === bindingName
267+
(binding): binding is ExtractedBinding<Type> => {
268+
if (other) {
269+
for (const [k, v] of Object.entries(other)) {
270+
if (other[k as keyof ExtractedBinding<Type>] !== v) {
271+
return false;
272+
}
273+
}
274+
}
275+
return binding.type === type && binding.name === bindingName;
276+
}
227277
);
228278
}
229279

@@ -258,6 +308,7 @@ async function runProvisioningFlow(
258308
const SEARCH_OPTION_VALUE = "__WRANGLER_INTERNAL_SEARCH";
259309
const MAX_OPTIONS = 4;
260310
if (pending.length) {
311+
// NB preExisting does not actually contain all resources on the account - we max out at ~100
261312
const options = preExisting.slice(0, MAX_OPTIONS - 1);
262313
if (options.length < preExisting.length) {
263314
options.push({
@@ -268,67 +319,91 @@ async function runProvisioningFlow(
268319

269320
for (const item of pending) {
270321
logger.log("Provisioning", item.binding, `(${friendlyBindingName})...`);
271-
let name: string = "";
322+
let name = item.name;
272323
let selected: string;
273324

274-
if (options.length === 0 || autoCreate) {
275-
selected = NEW_OPTION_VALUE;
276-
} else {
277-
selected = await select(
278-
`Would you like to connect an existing ${friendlyBindingName} or create a new one?`,
279-
{
280-
choices: options.concat([{ title: "Create new", value: "new" }]),
281-
defaultOption: options.length,
325+
if (name) {
326+
logger.log("Resource name found in config:", name);
327+
// this would be a d1 database where the name is provided but
328+
// not the id, which must be connected to an existing resource
329+
// of that name (or a new one with that name). This should hit a
330+
// 'getDbByName' endpoint, as preExisting does not contain all
331+
// resources on the account. But that doesn't exist yet.
332+
const foundResourceId = preExisting.find(
333+
(r) => r.title === name
334+
)?.value;
335+
if (foundResourceId) {
336+
logger.log("Existing resource found with that name.");
337+
item.updateId(foundResourceId);
338+
} else {
339+
logger.log("No pre-existing resource found with that name");
340+
logger.debug(
341+
"If you have many resources, we may not have searched through them all. Please provide the id in that case. This is a temporary limitation."
342+
);
343+
const proceed = autoCreate
344+
? true
345+
: await confirm(
346+
`Would you like to create a new ${friendlyBindingName} named "${name}"?`
347+
);
348+
if (!proceed) {
349+
throw new UserError("Resource provisioning cancelled.");
282350
}
283-
);
284-
}
285-
286-
if (selected === NEW_OPTION_VALUE) {
287-
const defaultValue = `${scriptName}-${item.binding.toLowerCase().replace("_", "-")}`;
288-
name = autoCreate
289-
? defaultValue
290-
: await prompt(`Enter a name for your new ${friendlyBindingName}`, {
291-
defaultValue,
292-
});
293-
logger.log(`🌀 Creating new ${friendlyBindingName} "${name}"...`);
294-
// creates new resource and mutates `bindings` to update id
295-
await item.create(name);
296-
} else if (selected === SEARCH_OPTION_VALUE) {
297-
let searchedResource: NormalisedResourceInfo | undefined;
298-
while (searchedResource === undefined) {
299-
const input = await prompt(
300-
`Enter the ${resourceKeyDescriptor} for an existing ${friendlyBindingName}`
351+
logger.log(`🌀 Creating new ${friendlyBindingName} "${name}"...`);
352+
const id = await item.create(name);
353+
item.updateId(id);
354+
}
355+
} else {
356+
if (options.length === 0 || autoCreate) {
357+
selected = NEW_OPTION_VALUE;
358+
} else {
359+
selected = await select(
360+
`Would you like to connect an existing ${friendlyBindingName} or create a new one?`,
361+
{
362+
choices: options.concat([
363+
{ title: "Create new", value: NEW_OPTION_VALUE },
364+
]),
365+
defaultOption: options.length,
366+
}
301367
);
302-
searchedResource = preExisting.find((r) => {
303-
if (r.title === input || r.value === input) {
304-
name = r.title;
305-
item.updateId(r.value);
306-
return true;
368+
}
369+
if (selected === NEW_OPTION_VALUE) {
370+
const defaultValue = `${scriptName}-${item.binding.toLowerCase().replace("_", "-")}`;
371+
name = autoCreate
372+
? defaultValue
373+
: await prompt(`Enter a name for your new ${friendlyBindingName}`, {
374+
defaultValue,
375+
});
376+
logger.log(`🌀 Creating new ${friendlyBindingName} "${name}"...`);
377+
const id = await item.create(name);
378+
item.updateId(id);
379+
} else if (selected === SEARCH_OPTION_VALUE) {
380+
// search through pre-existing resources that weren't listed
381+
let foundResource: NormalisedResourceInfo | undefined;
382+
while (foundResource === undefined) {
383+
const input = await prompt(
384+
`Enter the ${resourceKeyDescriptor} for an existing ${friendlyBindingName}`
385+
);
386+
foundResource = preExisting.find(
387+
(r) => r.title === input || r.value === input
388+
);
389+
if (foundResource) {
390+
name = foundResource.title;
391+
item.updateId(foundResource.value);
307392
} else {
308-
return false;
393+
logger.log(
394+
`No ${friendlyBindingName} with that ${resourceKeyDescriptor} "${input}" found. Please try again.`
395+
);
309396
}
310-
});
311-
if (!searchedResource) {
312-
logger.log(
313-
`No ${friendlyBindingName} with that ${resourceKeyDescriptor} "${input}" found. Please try again.`
314-
);
315397
}
316-
}
317-
} else {
318-
const selectedResource = preExisting.find((r) => {
319-
if (r.value === selected) {
320-
name = r.title;
398+
} else {
399+
// directly select a listed, pre-existing resource
400+
const selectedResource = preExisting.find(
401+
(r) => r.value === selected
402+
);
403+
if (selectedResource) {
404+
name = selectedResource.title;
321405
item.updateId(selected);
322-
return true;
323-
} else {
324-
return false;
325406
}
326-
});
327-
// we shouldn't get here
328-
if (!selectedResource) {
329-
throw new FatalError(
330-
`${friendlyBindingName} with id ${selected} not found`
331-
);
332407
}
333408
}
334409

0 commit comments

Comments
 (0)
Please sign in to comment.