diff --git a/src/application/cli/command/init.ts b/src/application/cli/command/init.ts index 205be51a..b76c7a5c 100644 --- a/src/application/cli/command/init.ts +++ b/src/application/cli/command/init.ts @@ -4,9 +4,9 @@ import {Output} from '@/application/cli/io/output'; import {Input} from '@/application/cli/io/input'; import {Sdk} from '@/application/project/sdk/sdk'; import {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration'; -import {OrganizationOptions} from '@/application/cli/form/organization/organizationForm'; +import {OrganizationOptions, SelectedOrganization} from '@/application/cli/form/organization/organizationForm'; import {ApplicationOptions} from '@/application/cli/form/application/applicationForm'; -import {WorkspaceOptions} from '@/application/cli/form/workspace/workspaceForm'; +import {SelectedWorkspace, WorkspaceOptions} from '@/application/cli/form/workspace/workspaceForm'; import {Form} from '@/application/cli/form/form'; import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager'; import {UserApi} from '@/application/api/user'; @@ -67,7 +67,7 @@ export class InitCommand implements Command { const {configurationManager, platformProvider, sdkProvider, io: {output}} = this.config; const currentConfiguration = input.override !== true && await configurationManager.isInitialized() - ? await configurationManager.loadPartial() + ? {...await configurationManager.loadPartial()} : null; const platform = await platformProvider.get(); @@ -88,28 +88,26 @@ export class InitCommand implements Command { const organization = await this.getOrganization( {new: input.new === 'organization'}, - input.organization ?? currentConfiguration?.organization, + input.new === 'organization' + ? undefined + : (input.organization ?? currentConfiguration?.organization), ); - if (organization === null) { - throw new HelpfulError(`Organization not found: ${input.organization}`, { - reason: ErrorReason.INVALID_INPUT, - }); - } - const workspace = await this.getWorkspace( { organization: organization, - new: input.new === 'workspace', + new: organization.new !== true && input.new === 'workspace', }, - input.workspace ?? currentConfiguration?.workspace, + (input.new === 'workspace' || organization.new === true) + ? undefined + : (input.workspace ?? currentConfiguration?.workspace), ); const applicationOptions: Omit = { organization: organization, workspace: workspace, platform: platform ?? Platform.JAVASCRIPT, - new: input.new === 'application', + new: workspace.new !== true && input.new === 'application', }; const devApplication = await this.getApplication( @@ -117,7 +115,9 @@ export class InitCommand implements Command { ...applicationOptions, environment: ApplicationEnvironment.DEVELOPMENT, }, - input.devApplication ?? currentConfiguration?.applications?.development, + (input.new !== undefined || workspace.new === true) + ? undefined + : (input.devApplication ?? currentConfiguration?.applications?.development), ); const updatedConfiguration: ProjectConfiguration = { @@ -133,17 +133,17 @@ export class InitCommand implements Command { paths: currentConfiguration?.paths ?? {}, }; - const defaultWebsite = workspace.website ?? organization.website ?? undefined; - - if (defaultWebsite !== undefined && new URL(defaultWebsite).hostname !== 'localhost') { - const prodApplication = await this.getApplication( - { - ...applicationOptions, - environment: ApplicationEnvironment.PRODUCTION, - }, - input.prodApplication ?? currentConfiguration?.applications?.production, - ); + const prodApplication = await this.resolveApplication( + { + ...applicationOptions, + environment: ApplicationEnvironment.PRODUCTION, + }, + input.new !== undefined + ? undefined + : (input.prodApplication ?? currentConfiguration?.applications?.production), + ); + if (prodApplication !== null) { updatedConfiguration.applications.production = prodApplication.slug; } @@ -170,7 +170,10 @@ export class InitCommand implements Command { ); } - private async getOrganization(options: OrganizationOptions, organizationSlug?: string): Promise { + private async getOrganization( + options: OrganizationOptions, + organizationSlug?: string, + ): Promise { const {form, api} = this.config; const organization = organizationSlug === undefined @@ -194,7 +197,7 @@ export class InitCommand implements Command { return organization; } - private async getWorkspace(options: WorkspaceOptions, workspaceSlug?: string): Promise { + private async getWorkspace(options: WorkspaceOptions, workspaceSlug?: string): Promise { const {form, api} = this.config; const workspace = workspaceSlug === undefined @@ -221,6 +224,40 @@ export class InitCommand implements Command { return workspace; } + private async resolveApplication(options: ApplicationOptions, applicationSlug?: string): Promise { + const {api} = this.config; + + const defaultWebsite = options.workspace.website ?? options.organization.website ?? undefined; + // Prod application can only be created if the default website is not localhost + const isPublicUrl = defaultWebsite !== undefined && new URL(defaultWebsite).hostname !== 'localhost'; + + if ( + (options.environment === ApplicationEnvironment.DEVELOPMENT || isPublicUrl) + || applicationSlug !== undefined + || options.new === true + ) { + // Continue to the regular flow if either creating a new application + // is possible (dev environment or public URL), a specific application slug is provided, + // or the user wants to create a new application. + return this.getApplication(options, applicationSlug); + } + + const applications = api.workspace.getApplications({ + organizationSlug: options.organization.slug, + workspaceSlug: options.workspace.slug, + }); + + for (const application of await applications) { + if (application.environment === options.environment) { + // There is an existing application for the specified environment, + // auto-select it or prompt the user to select it. + return this.getApplication(options, applicationSlug); + } + } + + return null; + } + private async getApplication(options: ApplicationOptions, applicationSlug?: string): Promise { const {form, api} = this.config; diff --git a/src/application/cli/form/application/applicationForm.ts b/src/application/cli/form/application/applicationForm.ts index 24c4e472..c0c4c023 100644 --- a/src/application/cli/form/application/applicationForm.ts +++ b/src/application/cli/form/application/applicationForm.ts @@ -24,14 +24,18 @@ export type ApplicationOptions = { default?: string, }; -export class ApplicationForm implements Form { +export type SelectedApplication = Application & { + new?: boolean, +}; + +export class ApplicationForm implements Form { private readonly config: Configuration; public constructor(config: Configuration) { this.config = config; } - public async handle(options: ApplicationOptions): Promise { + public async handle(options: ApplicationOptions): Promise { const {workspaceApi: api, output, input} = this.config; const {organization, workspace, environment} = options; @@ -73,7 +77,10 @@ export class ApplicationForm implements Form { }); } - private async setupApplication(options: ApplicationOptions, applications: Application[]): Promise { + private async setupApplication( + options: ApplicationOptions, + applications: Application[], + ): Promise { const {workspaceApi: api, output, input} = this.config; const {organization, workspace, platform, environment} = options; const customized = options.new === true; @@ -81,7 +88,9 @@ export class ApplicationForm implements Form { const name = customized ? await NameInput.prompt({ input: input, - label: 'Application name', + label: environment === ApplicationEnvironment.DEVELOPMENT + ? 'Development application name' + : 'Production application name', default: 'Website', validator: value => applications.every( app => app.name.toLowerCase() !== value.toLowerCase() || app.environment !== environment, @@ -94,7 +103,9 @@ export class ApplicationForm implements Form { const website = customized || !ApplicationForm.isValidUrl(defaultWebsite, environment) ? await UrlInput.prompt({ input: input, - label: 'Application website', + label: environment === ApplicationEnvironment.DEVELOPMENT + ? 'Development application URL' + : 'Production application URL', default: defaultWebsite, validate: url => { if (!URL.canParse(url)) { @@ -125,7 +136,10 @@ export class ApplicationForm implements Form { notifier.confirm(ApplicationForm.formatSelection(application)); - return application; + return { + ...application, + new: true, + }; } finally { notifier.stop(); } diff --git a/src/application/cli/form/organization/organizationForm.ts b/src/application/cli/form/organization/organizationForm.ts index 6a04c2e5..e87d080a 100644 --- a/src/application/cli/form/organization/organizationForm.ts +++ b/src/application/cli/form/organization/organizationForm.ts @@ -17,6 +17,10 @@ export type OrganizationOptions = { default?: string, }; +export type SelectedOrganization = Organization & { + new?: boolean, +}; + export class OrganizationForm implements Form { private readonly config: Configuration; @@ -24,7 +28,7 @@ export class OrganizationForm implements Form this.config = config; } - public async handle(options: OrganizationOptions = {}): Promise { + public async handle(options: OrganizationOptions = {}): Promise { const {userApi: api, output, input} = this.config; if (options.new !== true) { @@ -81,15 +85,18 @@ export class OrganizationForm implements Form const notifier = this.notify('Setting up organization'); try { - const resources = await api.setupOrganization({ + const organization = await api.setupOrganization({ website: website, locale: locale, timeZone: timeZone, }); - notifier.confirm(`Organization: ${resources.name}`); + notifier.confirm(`Organization: ${organization.name}`); - return resources; + return { + ...organization, + new: true, + }; } finally { notifier.stop(); } diff --git a/src/application/cli/form/workspace/workspaceForm.ts b/src/application/cli/form/workspace/workspaceForm.ts index 8b9641ab..4d41cb3c 100644 --- a/src/application/cli/form/workspace/workspaceForm.ts +++ b/src/application/cli/form/workspace/workspaceForm.ts @@ -19,6 +19,10 @@ export type WorkspaceOptions = { default?: string, }; +export type SelectedWorkspace = Workspace & { + new?: boolean, +}; + export class WorkspaceForm implements Form { private readonly config: Configuration; @@ -26,7 +30,7 @@ export class WorkspaceForm implements Form { this.config = config; } - public async handle(options: WorkspaceOptions): Promise { + public async handle(options: WorkspaceOptions): Promise { const {organizationApi: api, output, input} = this.config; const {organization} = options; @@ -63,7 +67,7 @@ export class WorkspaceForm implements Form { return this.setupWorkspace(organization, options.new === true); } - private async setupWorkspace(organization: Organization, customized: boolean): Promise { + private async setupWorkspace(organization: Organization, customized: boolean): Promise { const {organizationApi: api, input, output} = this.config; const name = customized @@ -90,7 +94,10 @@ export class WorkspaceForm implements Form { notifier.confirm(`Workspace: ${workspace.name}`); - return workspace; + return { + ...workspace, + new: customized, + }; } finally { notifier.stop(); } diff --git a/src/infrastructure/application/api/graphql/workspace.ts b/src/infrastructure/application/api/graphql/workspace.ts index dee4cb89..74cc195e 100644 --- a/src/infrastructure/application/api/graphql/workspace.ts +++ b/src/infrastructure/application/api/graphql/workspace.ts @@ -831,7 +831,7 @@ export class GraphqlWorkspaceApi implements WorkspaceApi { ): Promise { return generateAvailableSlug({ query: applicationSlugAvailabilityQuery, - baseName: `${baseName} ${environment.slice(0, 3).toLowerCase()}`, + baseName: `${baseName} ${environment === ApplicationEnvironment.DEVELOPMENT ? 'dev' : 'prod'}`, client: this.client, variables: { workspaceId: workspaceId,