Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 63 additions & 26 deletions src/application/cli/command/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -67,7 +67,7 @@ export class InitCommand implements Command<InitInput> {
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();
Expand All @@ -88,36 +88,36 @@ export class InitCommand implements Command<InitInput> {

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<ApplicationOptions, 'environment'> = {
organization: organization,
workspace: workspace,
platform: platform ?? Platform.JAVASCRIPT,
new: input.new === 'application',
new: workspace.new !== true && input.new === 'application',
};

const devApplication = await this.getApplication(
{
...applicationOptions,
environment: ApplicationEnvironment.DEVELOPMENT,
},
input.devApplication ?? currentConfiguration?.applications?.development,
(input.new !== undefined || workspace.new === true)
? undefined
: (input.devApplication ?? currentConfiguration?.applications?.development),
);

const updatedConfiguration: ProjectConfiguration = {
Expand All @@ -133,17 +133,17 @@ export class InitCommand implements Command<InitInput> {
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;
}

Expand All @@ -170,7 +170,10 @@ export class InitCommand implements Command<InitInput> {
);
}

private async getOrganization(options: OrganizationOptions, organizationSlug?: string): Promise<Organization> {
private async getOrganization(
options: OrganizationOptions,
organizationSlug?: string,
): Promise<SelectedOrganization> {
const {form, api} = this.config;

const organization = organizationSlug === undefined
Expand All @@ -194,7 +197,7 @@ export class InitCommand implements Command<InitInput> {
return organization;
}

private async getWorkspace(options: WorkspaceOptions, workspaceSlug?: string): Promise<Workspace> {
private async getWorkspace(options: WorkspaceOptions, workspaceSlug?: string): Promise<SelectedWorkspace> {
const {form, api} = this.config;

const workspace = workspaceSlug === undefined
Expand All @@ -221,6 +224,40 @@ export class InitCommand implements Command<InitInput> {
return workspace;
}

private async resolveApplication(options: ApplicationOptions, applicationSlug?: string): Promise<Application|null> {
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<Application> {
const {form, api} = this.config;

Expand Down
26 changes: 20 additions & 6 deletions src/application/cli/form/application/applicationForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ export type ApplicationOptions = {
default?: string,
};

export class ApplicationForm implements Form<Application, ApplicationOptions> {
export type SelectedApplication = Application & {
new?: boolean,
};

export class ApplicationForm implements Form<SelectedApplication, ApplicationOptions> {
private readonly config: Configuration;

public constructor(config: Configuration) {
this.config = config;
}

public async handle(options: ApplicationOptions): Promise<Application> {
public async handle(options: ApplicationOptions): Promise<SelectedApplication> {
const {workspaceApi: api, output, input} = this.config;
const {organization, workspace, environment} = options;

Expand Down Expand Up @@ -73,15 +77,20 @@ export class ApplicationForm implements Form<Application, ApplicationOptions> {
});
}

private async setupApplication(options: ApplicationOptions, applications: Application[]): Promise<Application> {
private async setupApplication(
options: ApplicationOptions,
applications: Application[],
): Promise<SelectedApplication> {
const {workspaceApi: api, output, input} = this.config;
const {organization, workspace, platform, environment} = options;
const customized = options.new === true;

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,
Expand All @@ -94,7 +103,9 @@ export class ApplicationForm implements Form<Application, ApplicationOptions> {
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)) {
Expand Down Expand Up @@ -125,7 +136,10 @@ export class ApplicationForm implements Form<Application, ApplicationOptions> {

notifier.confirm(ApplicationForm.formatSelection(application));

return application;
return {
...application,
new: true,
};
} finally {
notifier.stop();
}
Expand Down
15 changes: 11 additions & 4 deletions src/application/cli/form/organization/organizationForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ export type OrganizationOptions = {
default?: string,
};

export type SelectedOrganization = Organization & {
new?: boolean,
};

export class OrganizationForm implements Form<Organization, OrganizationOptions> {
private readonly config: Configuration;

public constructor(config: Configuration) {
this.config = config;
}

public async handle(options: OrganizationOptions = {}): Promise<Organization> {
public async handle(options: OrganizationOptions = {}): Promise<SelectedOrganization> {
const {userApi: api, output, input} = this.config;

if (options.new !== true) {
Expand Down Expand Up @@ -81,15 +85,18 @@ export class OrganizationForm implements Form<Organization, OrganizationOptions>
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();
}
Expand Down
13 changes: 10 additions & 3 deletions src/application/cli/form/workspace/workspaceForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ export type WorkspaceOptions = {
default?: string,
};

export type SelectedWorkspace = Workspace & {
new?: boolean,
};

export class WorkspaceForm implements Form<Workspace, WorkspaceOptions> {
private readonly config: Configuration;

public constructor(config: Configuration) {
this.config = config;
}

public async handle(options: WorkspaceOptions): Promise<Workspace> {
public async handle(options: WorkspaceOptions): Promise<SelectedWorkspace> {
const {organizationApi: api, output, input} = this.config;
const {organization} = options;

Expand Down Expand Up @@ -63,7 +67,7 @@ export class WorkspaceForm implements Form<Workspace, WorkspaceOptions> {
return this.setupWorkspace(organization, options.new === true);
}

private async setupWorkspace(organization: Organization, customized: boolean): Promise<Workspace> {
private async setupWorkspace(organization: Organization, customized: boolean): Promise<SelectedWorkspace> {
const {organizationApi: api, input, output} = this.config;

const name = customized
Expand All @@ -90,7 +94,10 @@ export class WorkspaceForm implements Form<Workspace, WorkspaceOptions> {

notifier.confirm(`Workspace: ${workspace.name}`);

return workspace;
return {
...workspace,
new: customized,
};
} finally {
notifier.stop();
}
Expand Down
2 changes: 1 addition & 1 deletion src/infrastructure/application/api/graphql/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ export class GraphqlWorkspaceApi implements WorkspaceApi {
): Promise<string> {
return generateAvailableSlug({
query: applicationSlugAvailabilityQuery,
baseName: `${baseName} ${environment.slice(0, 3).toLowerCase()}`,
baseName: `${baseName} ${environment === ApplicationEnvironment.DEVELOPMENT ? 'dev' : 'prod'}`,
client: this.client,
variables: {
workspaceId: workspaceId,
Expand Down
Loading