Skip to content

Commit 28ad98a

Browse files
authored
Fix init command (#112)
1 parent 39e4956 commit 28ad98a

File tree

5 files changed

+105
-40
lines changed

5 files changed

+105
-40
lines changed

src/application/cli/command/init.ts

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import {Output} from '@/application/cli/io/output';
44
import {Input} from '@/application/cli/io/input';
55
import {Sdk} from '@/application/project/sdk/sdk';
66
import {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration';
7-
import {OrganizationOptions} from '@/application/cli/form/organization/organizationForm';
7+
import {OrganizationOptions, SelectedOrganization} from '@/application/cli/form/organization/organizationForm';
88
import {ApplicationOptions} from '@/application/cli/form/application/applicationForm';
9-
import {WorkspaceOptions} from '@/application/cli/form/workspace/workspaceForm';
9+
import {SelectedWorkspace, WorkspaceOptions} from '@/application/cli/form/workspace/workspaceForm';
1010
import {Form} from '@/application/cli/form/form';
1111
import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager';
1212
import {UserApi} from '@/application/api/user';
@@ -67,7 +67,7 @@ export class InitCommand implements Command<InitInput> {
6767
const {configurationManager, platformProvider, sdkProvider, io: {output}} = this.config;
6868

6969
const currentConfiguration = input.override !== true && await configurationManager.isInitialized()
70-
? await configurationManager.loadPartial()
70+
? {...await configurationManager.loadPartial()}
7171
: null;
7272

7373
const platform = await platformProvider.get();
@@ -88,36 +88,36 @@ export class InitCommand implements Command<InitInput> {
8888

8989
const organization = await this.getOrganization(
9090
{new: input.new === 'organization'},
91-
input.organization ?? currentConfiguration?.organization,
91+
input.new === 'organization'
92+
? undefined
93+
: (input.organization ?? currentConfiguration?.organization),
9294
);
9395

94-
if (organization === null) {
95-
throw new HelpfulError(`Organization not found: ${input.organization}`, {
96-
reason: ErrorReason.INVALID_INPUT,
97-
});
98-
}
99-
10096
const workspace = await this.getWorkspace(
10197
{
10298
organization: organization,
103-
new: input.new === 'workspace',
99+
new: organization.new !== true && input.new === 'workspace',
104100
},
105-
input.workspace ?? currentConfiguration?.workspace,
101+
(input.new === 'workspace' || organization.new === true)
102+
? undefined
103+
: (input.workspace ?? currentConfiguration?.workspace),
106104
);
107105

108106
const applicationOptions: Omit<ApplicationOptions, 'environment'> = {
109107
organization: organization,
110108
workspace: workspace,
111109
platform: platform ?? Platform.JAVASCRIPT,
112-
new: input.new === 'application',
110+
new: workspace.new !== true && input.new === 'application',
113111
};
114112

115113
const devApplication = await this.getApplication(
116114
{
117115
...applicationOptions,
118116
environment: ApplicationEnvironment.DEVELOPMENT,
119117
},
120-
input.devApplication ?? currentConfiguration?.applications?.development,
118+
(input.new !== undefined || workspace.new === true)
119+
? undefined
120+
: (input.devApplication ?? currentConfiguration?.applications?.development),
121121
);
122122

123123
const updatedConfiguration: ProjectConfiguration = {
@@ -133,17 +133,17 @@ export class InitCommand implements Command<InitInput> {
133133
paths: currentConfiguration?.paths ?? {},
134134
};
135135

136-
const defaultWebsite = workspace.website ?? organization.website ?? undefined;
137-
138-
if (defaultWebsite !== undefined && new URL(defaultWebsite).hostname !== 'localhost') {
139-
const prodApplication = await this.getApplication(
140-
{
141-
...applicationOptions,
142-
environment: ApplicationEnvironment.PRODUCTION,
143-
},
144-
input.prodApplication ?? currentConfiguration?.applications?.production,
145-
);
136+
const prodApplication = await this.resolveApplication(
137+
{
138+
...applicationOptions,
139+
environment: ApplicationEnvironment.PRODUCTION,
140+
},
141+
input.new !== undefined
142+
? undefined
143+
: (input.prodApplication ?? currentConfiguration?.applications?.production),
144+
);
146145

146+
if (prodApplication !== null) {
147147
updatedConfiguration.applications.production = prodApplication.slug;
148148
}
149149

@@ -170,7 +170,10 @@ export class InitCommand implements Command<InitInput> {
170170
);
171171
}
172172

173-
private async getOrganization(options: OrganizationOptions, organizationSlug?: string): Promise<Organization> {
173+
private async getOrganization(
174+
options: OrganizationOptions,
175+
organizationSlug?: string,
176+
): Promise<SelectedOrganization> {
174177
const {form, api} = this.config;
175178

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

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

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

227+
private async resolveApplication(options: ApplicationOptions, applicationSlug?: string): Promise<Application|null> {
228+
const {api} = this.config;
229+
230+
const defaultWebsite = options.workspace.website ?? options.organization.website ?? undefined;
231+
// Prod application can only be created if the default website is not localhost
232+
const isPublicUrl = defaultWebsite !== undefined && new URL(defaultWebsite).hostname !== 'localhost';
233+
234+
if (
235+
(options.environment === ApplicationEnvironment.DEVELOPMENT || isPublicUrl)
236+
|| applicationSlug !== undefined
237+
|| options.new === true
238+
) {
239+
// Continue to the regular flow if either creating a new application
240+
// is possible (dev environment or public URL), a specific application slug is provided,
241+
// or the user wants to create a new application.
242+
return this.getApplication(options, applicationSlug);
243+
}
244+
245+
const applications = api.workspace.getApplications({
246+
organizationSlug: options.organization.slug,
247+
workspaceSlug: options.workspace.slug,
248+
});
249+
250+
for (const application of await applications) {
251+
if (application.environment === options.environment) {
252+
// There is an existing application for the specified environment,
253+
// auto-select it or prompt the user to select it.
254+
return this.getApplication(options, applicationSlug);
255+
}
256+
}
257+
258+
return null;
259+
}
260+
224261
private async getApplication(options: ApplicationOptions, applicationSlug?: string): Promise<Application> {
225262
const {form, api} = this.config;
226263

src/application/cli/form/application/applicationForm.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@ export type ApplicationOptions = {
2424
default?: string,
2525
};
2626

27-
export class ApplicationForm implements Form<Application, ApplicationOptions> {
27+
export type SelectedApplication = Application & {
28+
new?: boolean,
29+
};
30+
31+
export class ApplicationForm implements Form<SelectedApplication, ApplicationOptions> {
2832
private readonly config: Configuration;
2933

3034
public constructor(config: Configuration) {
3135
this.config = config;
3236
}
3337

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

@@ -73,15 +77,20 @@ export class ApplicationForm implements Form<Application, ApplicationOptions> {
7377
});
7478
}
7579

76-
private async setupApplication(options: ApplicationOptions, applications: Application[]): Promise<Application> {
80+
private async setupApplication(
81+
options: ApplicationOptions,
82+
applications: Application[],
83+
): Promise<SelectedApplication> {
7784
const {workspaceApi: api, output, input} = this.config;
7885
const {organization, workspace, platform, environment} = options;
7986
const customized = options.new === true;
8087

8188
const name = customized
8289
? await NameInput.prompt({
8390
input: input,
84-
label: 'Application name',
91+
label: environment === ApplicationEnvironment.DEVELOPMENT
92+
? 'Development application name'
93+
: 'Production application name',
8594
default: 'Website',
8695
validator: value => applications.every(
8796
app => app.name.toLowerCase() !== value.toLowerCase() || app.environment !== environment,
@@ -94,7 +103,9 @@ export class ApplicationForm implements Form<Application, ApplicationOptions> {
94103
const website = customized || !ApplicationForm.isValidUrl(defaultWebsite, environment)
95104
? await UrlInput.prompt({
96105
input: input,
97-
label: 'Application website',
106+
label: environment === ApplicationEnvironment.DEVELOPMENT
107+
? 'Development application URL'
108+
: 'Production application URL',
98109
default: defaultWebsite,
99110
validate: url => {
100111
if (!URL.canParse(url)) {
@@ -125,7 +136,10 @@ export class ApplicationForm implements Form<Application, ApplicationOptions> {
125136

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

128-
return application;
139+
return {
140+
...application,
141+
new: true,
142+
};
129143
} finally {
130144
notifier.stop();
131145
}

src/application/cli/form/organization/organizationForm.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ export type OrganizationOptions = {
1717
default?: string,
1818
};
1919

20+
export type SelectedOrganization = Organization & {
21+
new?: boolean,
22+
};
23+
2024
export class OrganizationForm implements Form<Organization, OrganizationOptions> {
2125
private readonly config: Configuration;
2226

2327
public constructor(config: Configuration) {
2428
this.config = config;
2529
}
2630

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

3034
if (options.new !== true) {
@@ -81,15 +85,18 @@ export class OrganizationForm implements Form<Organization, OrganizationOptions>
8185
const notifier = this.notify('Setting up organization');
8286

8387
try {
84-
const resources = await api.setupOrganization({
88+
const organization = await api.setupOrganization({
8589
website: website,
8690
locale: locale,
8791
timeZone: timeZone,
8892
});
8993

90-
notifier.confirm(`Organization: ${resources.name}`);
94+
notifier.confirm(`Organization: ${organization.name}`);
9195

92-
return resources;
96+
return {
97+
...organization,
98+
new: true,
99+
};
93100
} finally {
94101
notifier.stop();
95102
}

src/application/cli/form/workspace/workspaceForm.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@ export type WorkspaceOptions = {
1919
default?: string,
2020
};
2121

22+
export type SelectedWorkspace = Workspace & {
23+
new?: boolean,
24+
};
25+
2226
export class WorkspaceForm implements Form<Workspace, WorkspaceOptions> {
2327
private readonly config: Configuration;
2428

2529
public constructor(config: Configuration) {
2630
this.config = config;
2731
}
2832

29-
public async handle(options: WorkspaceOptions): Promise<Workspace> {
33+
public async handle(options: WorkspaceOptions): Promise<SelectedWorkspace> {
3034
const {organizationApi: api, output, input} = this.config;
3135
const {organization} = options;
3236

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

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

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

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

93-
return workspace;
97+
return {
98+
...workspace,
99+
new: customized,
100+
};
94101
} finally {
95102
notifier.stop();
96103
}

src/infrastructure/application/api/graphql/workspace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ export class GraphqlWorkspaceApi implements WorkspaceApi {
831831
): Promise<string> {
832832
return generateAvailableSlug({
833833
query: applicationSlugAvailabilityQuery,
834-
baseName: `${baseName} ${environment.slice(0, 3).toLowerCase()}`,
834+
baseName: `${baseName} ${environment === ApplicationEnvironment.DEVELOPMENT ? 'dev' : 'prod'}`,
835835
client: this.client,
836836
variables: {
837837
workspaceId: workspaceId,

0 commit comments

Comments
 (0)