From e123899855343114c5df2acebbe74a40d89ff23a Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Fri, 24 Jan 2025 21:58:17 +0000 Subject: [PATCH] refactor and add tests --- fixtures/worker-ts/wrangler.toml | 4 + .../wrangler/src/__tests__/provision.test.ts | 116 ++-- .../wrangler/src/api/startDevWorker/types.ts | 16 +- packages/wrangler/src/d1/utils.ts | 2 +- .../src/deployment-bundle/bindings.ts | 645 ++++++++++-------- 5 files changed, 451 insertions(+), 332 deletions(-) diff --git a/fixtures/worker-ts/wrangler.toml b/fixtures/worker-ts/wrangler.toml index 0404b911ae95e..fd4ebf7868163 100644 --- a/fixtures/worker-ts/wrangler.toml +++ b/fixtures/worker-ts/wrangler.toml @@ -1,3 +1,7 @@ name = "worker-ts" main = "src/index.ts" compatibility_date = "2023-05-04" + +[[d1_databases]] +binding = "HELLO_D2" +database_name = "lolkbhj" \ No newline at end of file diff --git a/packages/wrangler/src/__tests__/provision.test.ts b/packages/wrangler/src/__tests__/provision.test.ts index 4e66a1e416000..22bf55ea005bb 100644 --- a/packages/wrangler/src/__tests__/provision.test.ts +++ b/packages/wrangler/src/__tests__/provision.test.ts @@ -1,12 +1,7 @@ import { http, HttpResponse } from "msw"; import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; import { mockConsoleMethods } from "./helpers/mock-console"; -import { - clearDialogs, - mockConfirm, - mockPrompt, - mockSelect, -} from "./helpers/mock-dialogs"; +import { clearDialogs, mockPrompt, mockSelect } from "./helpers/mock-dialogs"; import { useMockIsTTY } from "./helpers/mock-istty"; import { mockCreateKVNamespace, @@ -190,19 +185,13 @@ describe("--x-provision", () => { - R2 Provisioning KV (KV Namespace)... - ✨ KV provisioned with test-kv - - -------------------------------------- + ✨ KV provisioned πŸŽ‰ Provisioning D1 (D1 Database)... - ✨ D1 provisioned with db-name - - -------------------------------------- + ✨ D1 provisioned πŸŽ‰ Provisioning R2 (R2 Bucket)... - ✨ R2 provisioned with existing-bucket-name - - -------------------------------------- + ✨ R2 provisioned πŸŽ‰ πŸŽ‰ All resources provisioned, continuing with deployment... @@ -315,19 +304,13 @@ describe("--x-provision", () => { - R2 Provisioning KV (KV Namespace)... - ✨ KV provisioned with test-kv-1 - - -------------------------------------- + ✨ KV provisioned πŸŽ‰ Provisioning D1 (D1 Database)... - ✨ D1 provisioned with test-d1-1 - - -------------------------------------- + ✨ D1 provisioned πŸŽ‰ Provisioning R2 (R2 Bucket)... - ✨ R2 provisioned with existing-bucket-1 - - -------------------------------------- + ✨ R2 provisioned πŸŽ‰ πŸŽ‰ All resources provisioned, continuing with deployment... @@ -451,21 +434,15 @@ describe("--x-provision", () => { Provisioning KV (KV Namespace)... πŸŒ€ Creating new KV Namespace \\"new-kv\\"... - ✨ KV provisioned with new-kv - - -------------------------------------- + ✨ KV provisioned πŸŽ‰ Provisioning D1 (D1 Database)... πŸŒ€ Creating new D1 Database \\"new-d1\\"... - ✨ D1 provisioned with new-d1 - - -------------------------------------- + ✨ D1 provisioned πŸŽ‰ Provisioning R2 (R2 Bucket)... πŸŒ€ Creating new R2 Bucket \\"new-r2\\"... - ✨ R2 provisioned with new-r2 - - -------------------------------------- + ✨ R2 provisioned πŸŽ‰ πŸŽ‰ All resources provisioned, continuing with deployment... @@ -532,11 +509,8 @@ describe("--x-provision", () => { Provisioning D1 (D1 Database)... Resource name found in config: prefilled-d1-name - No pre-existing resource found with that name πŸŒ€ Creating new D1 Database \\"prefilled-d1-name\\"... - ✨ D1 provisioned with prefilled-d1-name - - -------------------------------------- + ✨ D1 provisioned πŸŽ‰ πŸŽ‰ All resources provisioned, continuing with deployment... @@ -653,11 +627,8 @@ describe("--x-provision", () => { Provisioning D1 (D1 Database)... Resource name found in config: new-d1-name - No pre-existing resource found with that name πŸŒ€ Creating new D1 Database \\"new-d1-name\\"... - ✨ D1 provisioned with new-d1-name - - -------------------------------------- + ✨ D1 provisioned πŸŽ‰ πŸŽ‰ All resources provisioned, continuing with deployment... @@ -730,9 +701,7 @@ describe("--x-provision", () => { Provisioning BUCKET (R2 Bucket)... Resource name found in config: prefilled-r2-name πŸŒ€ Creating new R2 Bucket \\"prefilled-r2-name\\"... - ✨ BUCKET provisioned with prefilled-r2-name - - -------------------------------------- + ✨ BUCKET provisioned πŸŽ‰ πŸŽ‰ All resources provisioned, continuing with deployment... @@ -749,8 +718,7 @@ describe("--x-provision", () => { expect(std.warn).toMatchInlineSnapshot(`""`); }); - // to maintain current behaviour - it("wont prompt to provision if an r2 bucket name belongs to an existing bucket", async () => { + it("won't prompt to provision if an r2 bucket name belongs to an existing bucket", async () => { writeWranglerConfig({ main: "index.js", r2_buckets: [ @@ -804,6 +772,58 @@ describe("--x-provision", () => { expect(std.warn).toMatchInlineSnapshot(`""`); }); + it("won't prompt to provision if a D1 database name belongs to an existing database", async () => { + writeWranglerConfig({ + main: "index.js", + d1_databases: [ + { + binding: "DB_NAME", + database_name: "existing-db-name", + }, + ], + }); + mockGetSettings(); + + msw.use( + http.get("*/accounts/:accountId/d1/database", async () => { + return HttpResponse.json( + createFetchResult([ + { + name: "existing-db-name", + uuid: "existing-d1-id", + }, + ]) + ); + }) + ); + + mockUploadWorkerRequest({ + expectedBindings: [ + { + name: "DB_NAME", + type: "d1", + id: "existing-d1-id", + }, + ], + }); + + await runWrangler("deploy --x-provision"); + + expect(std.out).toMatchInlineSnapshot(` + "Total Upload: xx KiB / gzip: xx KiB + Worker Startup Time: 100 ms + Your worker has access to the following bindings: + - D1 Databases: + - DB_NAME: existing-db-name (existing-d1-id) + Uploaded test-name (TIMINGS) + Deployed test-name triggers (TIMINGS) + https://test-name.test-sub-domain.workers.dev + Current Version ID: Galaxy-Class" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + // because buckets with the same name can exist in different jurisdictions it("will provision if the jurisdiction changes", async () => { writeWranglerConfig({ @@ -871,9 +891,7 @@ describe("--x-provision", () => { Provisioning BUCKET (R2 Bucket)... Resource name found in config: existing-bucket-name πŸŒ€ Creating new R2 Bucket \\"existing-bucket-name\\"... - ✨ BUCKET provisioned with existing-bucket-name - - -------------------------------------- + ✨ BUCKET provisioned πŸŽ‰ πŸŽ‰ All resources provisioned, continuing with deployment... diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index 6dcb6bc5fdf9e..2b5575f8c386c 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -247,15 +247,15 @@ export type Binding = | ({ type: "workflow" } & BindingOmit) | ({ type: "queue" } & BindingOmit) | ({ type: "r2_bucket" } & BindingOmit) - | ({ type: "d1" } & Omit) - | ({ type: "vectorize" } & Omit) - | ({ type: "hyperdrive" } & Omit) - | ({ type: "service" } & Omit) + | ({ type: "d1" } & BindingOmit) + | ({ type: "vectorize" } & BindingOmit) + | ({ type: "hyperdrive" } & BindingOmit) + | ({ type: "service" } & BindingOmit) | { type: "fetcher"; fetcher: ServiceFetch } - | ({ type: "analytics_engine" } & Omit) - | ({ type: "dispatch_namespace" } & Omit) - | ({ type: "mtls_certificate" } & Omit) - | ({ type: "pipeline" } & Omit) + | ({ type: "analytics_engine" } & BindingOmit) + | ({ type: "dispatch_namespace" } & BindingOmit) + | ({ type: "mtls_certificate" } & BindingOmit) + | ({ type: "pipeline" } & BindingOmit) | ({ type: "logfwdr" } & NameOmit) | { type: `unsafe_${string}` } | { type: "assets" }; diff --git a/packages/wrangler/src/d1/utils.ts b/packages/wrangler/src/d1/utils.ts index fc5bf161e54f0..89ed5a453f314 100644 --- a/packages/wrangler/src/d1/utils.ts +++ b/packages/wrangler/src/d1/utils.ts @@ -46,7 +46,7 @@ export const getDatabaseByNameOrBinding = async ( return dbFromConfig; } - const allDBs = await listDatabases(accountId); + const allDBs = await listDatabases(accountId, true); const matchingDB = allDBs.find((db) => db.name === name); if (!matchingDB) { throw new UserError(`Couldn't find DB with name '${name}'`); diff --git a/packages/wrangler/src/deployment-bundle/bindings.ts b/packages/wrangler/src/deployment-bundle/bindings.ts index 48eb67b51a5da..518f3f60e51b6 100644 --- a/packages/wrangler/src/deployment-bundle/bindings.ts +++ b/packages/wrangler/src/deployment-bundle/bindings.ts @@ -1,9 +1,8 @@ import assert from "node:assert"; -import chalk from "chalk"; import { fetchResult } from "../cfetch"; import { createD1Database } from "../d1/create"; import { listDatabases } from "../d1/list"; -import { getDatabaseInfoFromId } from "../d1/utils"; +import { getDatabaseByNameOrBinding, getDatabaseInfoFromId } from "../d1/utils"; import { prompt, select } from "../dialogs"; import { UserError } from "../errors"; import { createKVNamespace, listKVNamespaces } from "../kv/helpers"; @@ -78,29 +77,268 @@ export type Settings = { bindings: Array; }; -type PendingResourceOperations = { - // name may be provided in config without the resource having been provisioned - name?: string | undefined; - create: (name: string) => Promise; - updateId: (id: string) => void; +abstract class ProvisionResourceHandler< + T extends WorkerMetadataBinding["type"], + B extends CfD1Database | CfR2Bucket | CfKvNamespace, +> { + constructor( + public type: T, + public binding: B, + public idField: keyof B, + public accountId: string + ) {} + + abstract alreadyPresent( + settings: Settings | undefined + ): boolean | Promise; + + inherit(): void { + // @ts-expect-error idField is a key of this.binding + this.binding[this.idField] = INHERIT_SYMBOL; + } + connect(id: string): void { + // @ts-expect-error idField is a key of this.binding + this.binding[this.idField] = id; + } + + abstract create(name: string): Promise; + + abstract get name(): string | undefined; + + async provision(name: string): Promise { + const id = await this.create(name); + this.connect(id); + } + + // This binding is fully specified and can't/shouldn't be provisioned + // This is usually when it has an id (e.g. D1 `database_id`) + isFullySpecified(): boolean { + return false; + } + + // Does this binding need to be provisioned? + // Some bindings are not fully specified, but don't need provisioning + // (e.g. R2 binding, with a bucket_name that already exists) + // This + async isConnectedToExistingResource(): Promise { + return false; + } + + async shouldProvision(settings: Settings | undefined) { + if (!this.isFullySpecified()) { + if (await this.alreadyPresent(settings)) { + this.inherit(); + } else { + const connected = await this.isConnectedToExistingResource(); + if (connected) { + if (typeof connected === "string") { + this.connect(connected); + } + return false; + } + return true; + } + } + } +} + +class R2Handler extends ProvisionResourceHandler<"r2_bucket", CfR2Bucket> { + get name(): string | undefined { + return this.binding.bucket_name as string; + } + async create(name: string) { + await createR2Bucket( + this.accountId, + name, + undefined, + this.binding.jurisdiction + ); + return name; + } + constructor(binding: CfR2Bucket, accountId: string) { + super("r2_bucket", binding, "bucket_name", accountId); + } + alreadyPresent(settings: Settings | undefined): boolean { + return !!settings?.bindings.find( + (existing) => + existing.type === this.type && + existing.name === this.binding.binding && + existing.jurisdiction === this.binding.jurisdiction + ); + } + async isConnectedToExistingResource(): Promise { + assert(typeof this.binding.bucket_name !== "symbol"); + + // If the user hasn't specified a bucket_name in config, we always provision + if (!this.binding.bucket_name) { + return false; + } + try { + await getR2Bucket( + this.accountId, + this.binding.bucket_name, + this.binding.jurisdiction + ); + // This bucket_name exists! We don't need to provision it + return true; + } catch (e) { + if (!(e instanceof APIError && e.code === 10006)) { + // this is an error that is not "bucket not found", so we do want to throw + throw e; + } + + // This bucket_name doesn't existβ€”let's provision + return false; + } + } +} + +class KVHandler extends ProvisionResourceHandler< + "kv_namespace", + CfKvNamespace +> { + get name(): string | undefined { + return undefined; + } + async create(name: string) { + return await createKVNamespace(this.accountId, name); + } + constructor(binding: CfKvNamespace, accountId: string) { + super("kv_namespace", binding, "id", accountId); + } + alreadyPresent(settings: Settings | undefined): boolean { + return !!settings?.bindings.find( + (existing) => + existing.type === this.type && existing.name === this.binding.binding + ); + } + isFullySpecified(): boolean { + return !!this.binding.id; + } +} + +class D1Handler extends ProvisionResourceHandler<"d1", CfD1Database> { + get name(): string | undefined { + return this.binding.database_name as string; + } + async create(name: string) { + const db = await createD1Database(this.accountId, name); + return db.uuid; + } + constructor(binding: CfD1Database, accountId: string) { + super("d1", binding, "database_id", accountId); + } + async alreadyPresent(settings: Settings | undefined): Promise { + const maybeInherited = settings?.bindings.find( + (existing) => + existing.type === this.type && existing.name === this.binding.binding + ) as Extract | undefined; + // A D1 binding with the same binding name exists is already present on the worker... + if (maybeInherited) { + // ...and the user hasn't specified a name in their config, so we don't need to check if the database_name matches + if (!this.binding.database_name) { + return true; + } + + // ...and the user HAS specified a name in their config, so we need to check if the database_name they provided + // matches the database_name of the existing binding (which isn't present in settings, so we'll need to make an API call to check) + const dbFromId = await getDatabaseInfoFromId( + this.accountId, + maybeInherited.id + ); + if (this.binding.database_name === dbFromId.name) { + return true; + } + } + return false; + } + async isConnectedToExistingResource(): Promise { + assert(typeof this.binding.database_name !== "symbol"); + + // If the user hasn't specified a database_name in config, we always provision + if (!this.binding.database_name) { + return false; + } + try { + // TODO: Use https://jira.cfdata.org/browse/CFSQL-1180 once ready + const db = await getDatabaseByNameOrBinding( + { d1_databases: [] } as unknown as Config, + this.accountId, + this.binding.database_name + ); + // This database_name exists! We don't need to provision it + return db.uuid; + } catch (e) { + if ( + !( + e instanceof Error && + e.message.startsWith("Couldn't find DB with name") + ) + ) { + // this is an error that is not "database not found", so we do want to throw + throw e; + } + + // This database_name doesn't existβ€”let's provision + return false; + } + } + isFullySpecified(): boolean { + return !!this.binding.database_id; + } +} + +const HANDLERS = { + kv_namespaces: { + Handler: KVHandler, + sort: 0, + name: "KV Namespace", + keyDescription: "title or id", + }, + + d1_databases: { + Handler: D1Handler, + sort: 1, + name: "D1 Database", + keyDescription: "name or id", + }, + r2_buckets: { + Handler: R2Handler, + sort: 2, + name: "R2 Bucket", + keyDescription: "name", + }, }; -type PendingResources = { - kv_namespaces: (CfKvNamespace & PendingResourceOperations)[]; - r2_buckets: (CfR2Bucket & PendingResourceOperations)[]; - d1_databases: (CfD1Database & PendingResourceOperations)[]; + +const LOADERS = { + kv_namespaces: async (accountId: string) => { + const preExistingKV = await listKVNamespaces(accountId, true); + return preExistingKV.map((ns) => ({ title: ns.title, value: ns.id })); + }, + d1_databases: async (accountId: string) => { + const preExisting = await listDatabases(accountId, true, 1000); + return preExisting.map((db) => ({ title: db.name, value: db.uuid })); + }, + r2_buckets: async (accountId: string) => { + const preExisting = await listR2Buckets(accountId); + return preExisting.map((bucket) => ({ + title: bucket.name, + value: bucket.name, + })); + }, }; -export async function provisionBindings( - bindings: CfWorkerInit["bindings"], + +type PendingResource = { + binding: string; + resourceType: "kv_namespaces" | "d1_databases" | "r2_buckets"; + handler: KVHandler | D1Handler | R2Handler; +}; + +async function collectPendingResources( accountId: string, scriptName: string, - autoCreate: boolean, - config: Config -): Promise { - const pendingResources: PendingResources = { - d1_databases: [], - r2_buckets: [], - kv_namespaces: [], - }; + bindings: CfWorkerInit["bindings"] +): Promise { let settings: Settings | undefined; try { @@ -109,178 +347,80 @@ export async function provisionBindings( logger.debug("No settings found"); } - for (const kv of bindings.kv_namespaces ?? []) { - if (!kv.id) { - if (inBindingSettings(settings, "kv_namespace", kv.binding)) { - kv.id = INHERIT_SYMBOL; - } else { - pendingResources.kv_namespaces?.push({ - binding: kv.binding, - async create(title) { - const id = await createKVNamespace(accountId, title); - return id; - }, - updateId(id) { - kv.id = id; - }, - }); - } - } - } + const pendingResources: PendingResource[] = []; - for (const r2 of bindings.r2_buckets ?? []) { - assert(typeof r2.bucket_name !== "symbol"); - if ( - inBindingSettings(settings, "r2_bucket", r2.binding, { - bucket_name: r2.bucket_name, - jurisdiction: r2.jurisdiction, - }) - ) { - // does not inherit if the bucket name has changed - r2.bucket_name = INHERIT_SYMBOL; - } else { - if (r2.bucket_name) { - try { - await getR2Bucket(accountId, r2.bucket_name, r2.jurisdiction); - // don't provision - continue; - } catch (e) { - console.dir("here?"); - // bucket not found - provision - if (!(e instanceof APIError && e.code === 10006)) { - // this is an error that is not "bucket not found", so we do want to throw - throw e; - } - } - } - pendingResources.r2_buckets?.push({ - binding: r2.binding, - name: r2.bucket_name, - async create(bucketName) { - await createR2Bucket( - accountId, - bucketName, - undefined, - // respect jurisdiction if it has been specified in the config, but don't prompt - r2.jurisdiction - ); - return bucketName; - }, - updateId(bucketName) { - r2.bucket_name = bucketName; - }, - }); - } + try { + settings = await getSettings(accountId, scriptName); + } catch (error) { + logger.debug("No settings found"); } + for (const resourceType of Object.keys( + HANDLERS + ) as (keyof typeof HANDLERS)[]) { + for (const resource of bindings[resourceType] ?? []) { + const h = new HANDLERS[resourceType].Handler(resource, accountId); - for (const d1 of bindings.d1_databases ?? []) { - if (!d1.database_id) { - const maybeInherited = inBindingSettings(settings, "d1", d1.binding); - if (maybeInherited) { - if (!d1.database_name) { - d1.database_id = INHERIT_SYMBOL; - continue; - } else { - // check that the database name matches the id of the inherited binding - const dbFromId = await getDatabaseInfoFromId( - accountId, - maybeInherited.id - ); - if (d1.database_name === dbFromId.name) { - d1.database_id = INHERIT_SYMBOL; - continue; - } - } - // otherwise, db name has *changed* - re-provision + if (await h.shouldProvision(settings)) { + pendingResources.push({ + binding: resource.binding, + resourceType, + handler: h, + }); } - pendingResources.d1_databases?.push({ - binding: d1.binding, - name: d1.database_name, - async create(name) { - const db = await createD1Database(accountId, name); - return db.uuid; - }, - updateId(id) { - d1.database_id = id; - }, - }); } } - if (Object.values(pendingResources).some((v) => v && v.length > 0)) { + return pendingResources.sort( + (a, b) => HANDLERS[a.resourceType].sort - HANDLERS[b.resourceType].sort + ); +} +export async function provisionBindings( + bindings: CfWorkerInit["bindings"], + accountId: string, + scriptName: string, + autoCreate: boolean, + config: Config +): Promise { + const pendingResources = await collectPendingResources( + accountId, + scriptName, + bindings + ); + + if (pendingResources.length > 0) { if (!isLegacyEnv(config)) { throw new UserError( "Provisioning resources is not supported with a service environment" ); } logger.log(); - printBindings(pendingResources, { provisioning: true }); + const printable: Record = {}; + for (const resource of pendingResources) { + printable[resource.resourceType] ??= []; + printable[resource.resourceType].push({ binding: resource.binding }); + } + printBindings(printable, { provisioning: true }); logger.log(); - if (pendingResources.kv_namespaces?.length) { - const preExistingKV = await listKVNamespaces(accountId, true); - await runProvisioningFlow( - pendingResources.kv_namespaces, - preExistingKV.map((ns) => ({ title: ns.title, value: ns.id })), - "KV Namespace", - scriptName, - autoCreate - ); - } + const existingResources: Record = {}; + + for (const resource of pendingResources) { + existingResources[resource.resourceType] ??= + await LOADERS[resource.resourceType](accountId); - if (pendingResources.d1_databases?.length) { - const preExisting = await listDatabases(accountId, true, 1000); - await runProvisioningFlow( - pendingResources.d1_databases, - preExisting.map((db) => ({ title: db.name, value: db.uuid })), - "D1 Database", - scriptName, - autoCreate - ); - } - if (pendingResources.r2_buckets?.length) { - const preExisting = await listR2Buckets(accountId); await runProvisioningFlow( - pendingResources.r2_buckets, - preExisting.map((bucket) => ({ - title: bucket.name, - value: bucket.name, - })), - "R2 Bucket", + resource, + existingResources[resource.resourceType], + HANDLERS[resource.resourceType].name, scriptName, autoCreate ); } + logger.log(`πŸŽ‰ All resources provisioned, continuing with deployment...\n`); } } -/** checks whether the binding id can be inherited from a prev deployment */ -type ExtractedBinding = Extract; -function inBindingSettings( - settings: Settings | undefined, - type: Type, - bindingName: string, - other?: Partial, unknown>> -): ExtractedBinding | undefined { - return settings?.bindings.find( - (binding): binding is ExtractedBinding => { - if (other) { - for (const [k, v] of Object.entries(other)) { - // @ts-expect-error - if (v && binding[k] !== v) { - console.dir(k); - console.dir(v); - console.dir(binding); - return false; - } - } - } - return binding.type === type && binding.name === bindingName; - } - ); -} - function getSettings(accountId: string, scriptName: string) { return fetchResult( `/accounts/${accountId}/workers/scripts/${scriptName}/settings` @@ -289,8 +429,6 @@ function getSettings(accountId: string, scriptName: string) { function printDivider() { logger.log(); - logger.log(chalk.dim("--------------------------------------")); - logger.log(); } type NormalisedResourceInfo = { @@ -300,125 +438,84 @@ type NormalisedResourceInfo = { value: string; }; -type FriendlyBindingNames = "KV Namespace" | "D1 Database" | "R2 Bucket"; - async function runProvisioningFlow( - pending: PendingResources[keyof PendingResources], + item: PendingResource, preExisting: NormalisedResourceInfo[], - friendlyBindingName: FriendlyBindingNames, + friendlyBindingName: string, scriptName: string, autoCreate: boolean ) { const NEW_OPTION_VALUE = "__WRANGLER_INTERNAL_NEW"; const SEARCH_OPTION_VALUE = "__WRANGLER_INTERNAL_SEARCH"; const MAX_OPTIONS = 4; - if (pending.length) { - // NB preExisting does not actually contain all resources on the account - we max out at ~100 - const options = preExisting.slice(0, MAX_OPTIONS - 1); - if (options.length < preExisting.length) { - options.push({ - title: "Other (too many to list)", - value: SEARCH_OPTION_VALUE, - }); - } + // NB preExisting does not actually contain all resources on the account - we max out at ~100 + const options = preExisting.slice(0, MAX_OPTIONS - 1); + if (options.length < preExisting.length) { + options.push({ + title: "Other (too many to list)", + value: SEARCH_OPTION_VALUE, + }); + } - for (const item of pending) { - logger.log("Provisioning", item.binding, `(${friendlyBindingName})...`); - let name = item.name; - let selected: string; - - if (name) { - logger.log("Resource name found in config:", name); - // TODO: tidy this up :/// - // this would be a d1 database where the name is provided but - // not the id, which must be connected to an existing resource - // of that name (or a new one with that name). This should hit a - // 'getDbByName' endpoint, as preExisting does not contain all - // resources on the account. But that doesn't exist yet. - // this could also be an r2 bucket where the name is provided, and the jurisidiction has changed. - // todo, check if the bucket exists and if it does, update the jurisdiction. - // unfortunately, preExisting does not contain the jurisdiction for buckets - if (friendlyBindingName === "D1 Database") { - const foundResourceId = preExisting.find( - (r) => r.title === name - )?.value; - if (foundResourceId) { - logger.log("Existing resource found with that name."); - item.updateId(foundResourceId); - } else { - logger.log("No pre-existing resource found with that name"); - logger.log(`πŸŒ€ Creating new ${friendlyBindingName} "${name}"...`); - const id = await item.create(name); - item.updateId(id); - } - } else { - // r2 bucket - logger.log(`πŸŒ€ Creating new ${friendlyBindingName} "${name}"...`); - const id = await item.create(name); - item.updateId(id); + const defaultName = `${scriptName}-${item.binding.toLowerCase().replace("_", "-")}`; + logger.log("Provisioning", item.binding, `(${friendlyBindingName})...`); + + if (item.handler.name) { + logger.log("Resource name found in config:", item.handler.name); + logger.log( + `πŸŒ€ Creating new ${friendlyBindingName} "${item.handler.name}"...` + ); + await item.handler.provision(item.handler.name); + } else if (autoCreate) { + logger.log(`πŸŒ€ Creating new ${friendlyBindingName} "${defaultName}"...`); + await item.handler.provision(defaultName); + } else { + let action: string = NEW_OPTION_VALUE; + + if (options.length > 0) { + action = await select( + `Would you like to connect an existing ${friendlyBindingName} or create a new one?`, + { + choices: options.concat([ + { title: "Create new", value: NEW_OPTION_VALUE }, + ]), + defaultOption: options.length, } - } else { - if (options.length === 0 || autoCreate) { - selected = NEW_OPTION_VALUE; - } else { - selected = await select( - `Would you like to connect an existing ${friendlyBindingName} or create a new one?`, - { - choices: options.concat([ - { title: "Create new", value: NEW_OPTION_VALUE }, - ]), - defaultOption: options.length, - } - ); + ); + } + + if (action === NEW_OPTION_VALUE) { + const name = await prompt( + `Enter a name for your new ${friendlyBindingName}`, + { + defaultValue: defaultName, } - if (selected === NEW_OPTION_VALUE) { - const defaultValue = `${scriptName}-${item.binding.toLowerCase().replace("_", "-")}`; - name = autoCreate - ? defaultValue - : await prompt(`Enter a name for your new ${friendlyBindingName}`, { - defaultValue, - }); - logger.log(`πŸŒ€ Creating new ${friendlyBindingName} "${name}"...`); - const id = await item.create(name); - item.updateId(id); - } else if (selected === SEARCH_OPTION_VALUE) { - // search through pre-existing resources that weren't listed - let foundResource: NormalisedResourceInfo | undefined; - while (foundResource === undefined) { - const input = await prompt( - `Enter the ${resourceKeyDescriptors.get(friendlyBindingName)} for an existing ${friendlyBindingName}` - ); - foundResource = preExisting.find( - (r) => r.title === input || r.value === input - ); - if (foundResource) { - name = foundResource.title; - item.updateId(foundResource.value); - } else { - logger.log( - `No ${friendlyBindingName} with that ${resourceKeyDescriptors.get(friendlyBindingName)} "${input}" found. Please try again.` - ); - } - } + ); + logger.log(`πŸŒ€ Creating new ${friendlyBindingName} "${name}"...`); + await item.handler.provision(name); + } else if (action === SEARCH_OPTION_VALUE) { + // search through pre-existing resources that weren't listed + let foundResource: NormalisedResourceInfo | undefined; + while (foundResource === undefined) { + const input = await prompt( + `Enter the ${HANDLERS[item.resourceType].keyDescription} for an existing ${friendlyBindingName}` + ); + foundResource = preExisting.find( + (r) => r.title === input || r.value === input + ); + if (foundResource) { + item.handler.connect(foundResource.value); } else { - // directly select a listed, pre-existing resource - const selectedResource = preExisting.find( - (r) => r.value === selected + logger.log( + `No ${friendlyBindingName} with that ${HANDLERS[item.resourceType].keyDescription} "${input}" found. Please try again.` ); - if (selectedResource) { - name = selectedResource.title; - item.updateId(selected); - } } } - - logger.log(`✨ ${item.binding} provisioned with ${name}`); - printDivider(); + } else { + item.handler.connect(action); } } + + logger.log(`✨ ${item.binding} provisioned πŸŽ‰`); + printDivider(); } -const resourceKeyDescriptors = new Map([ - ["KV Namespace", "title or id"], - ["D1 Database", "name or id"], - ["R2 Bucket", "name"], -]);