Skip to content

Commit 8bf2df7

Browse files
New add env command (#1944)
1 parent 9a42b48 commit 8bf2df7

File tree

7 files changed

+136
-82
lines changed

7 files changed

+136
-82
lines changed

.changeset/smooth-rice-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ggt": minor
3+
---
4+
5+
New `ggt add env` command has been added to allow for environment creation.

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ Usage
243243

244244
ggt add field <model_path>/<field_name>:<field_type>
245245

246+
ggt add environment <env_name> [options]
247+
246248
Options
247249
-e, --env <env_name> Selects the environment to add to. Default set on ".gadget/sync.json"
248250

@@ -253,6 +255,9 @@ Examples
253255
Add a new model 'post' with 2 new 'string' type fields 'title' and 'body':
254256
$ ggt add model post title:string body:string
255257

258+
Add a new 'boolean' type field 'published' to an existing model
259+
ggt add field post/published:boolean
260+
256261
Add new action 'publish' to the 'post' model:
257262
ggt add action model/post/publish
258263

@@ -262,8 +267,8 @@ Examples
262267
Add a new route 'howdy'
263268
ggt add route GET howdy
264269

265-
Add a new 'boolean' type field 'published' to an existing model
266-
ggt add field post/published:boolean
270+
Clone the `development` environment into a new `staging` environment
271+
$ ggt add environment staging --environment development
267272
```
268273

269274
### `ggt open`

spec/commands/__snapshots__/root.spec.ts.snap

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Examples
2626
Add a new model 'post' with 2 new 'string' type fields 'title' and 'body':
2727
$ ggt add model post title:string body:string
2828
29+
Add a new 'boolean' type field 'published' to an existing model
30+
ggt add field post/published:boolean
31+
2932
Add new action 'publish' to the 'post' model:
3033
ggt add action model/post/publish
3134
@@ -35,8 +38,8 @@ Examples
3538
Add a new route 'howdy'
3639
ggt add route GET howdy
3740
38-
Add a new 'boolean' type field 'published' to an existing model
39-
ggt add field post/published:boolean
41+
Clone the \`development\` environment into a new \`staging\` environment
42+
ggt add environment staging --environment development
4043
"
4144
`;
4245
@@ -66,6 +69,9 @@ Examples
6669
Add a new model 'post' with 2 new 'string' type fields 'title' and 'body':
6770
$ ggt add model post title:string body:string
6871
72+
Add a new 'boolean' type field 'published' to an existing model
73+
ggt add field post/published:boolean
74+
6975
Add new action 'publish' to the 'post' model:
7076
ggt add action model/post/publish
7177
@@ -75,8 +81,8 @@ Examples
7581
Add a new route 'howdy'
7682
ggt add route GET howdy
7783
78-
Add a new 'boolean' type field 'published' to an existing model
79-
ggt add field post/published:boolean
84+
Clone the \`development\` environment into a new \`staging\` environment
85+
ggt add environment staging --environment development
8086
"
8187
`;
8288

spec/commands/add.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { beforeEach, describe, expect, it } from "vitest";
2+
import { EnvironmentStatus } from "../../src/__generated__/graphql.js";
23
import * as add from "../../src/commands/add.js";
34
import { GADGET_GLOBAL_ACTIONS_QUERY, GADGET_META_MODELS_QUERY } from "../../src/services/app/api/operation.js";
45
import {
56
CREATE_ACTION_MUTATION,
7+
CREATE_ENVIRONMENT_MUTATION,
68
CREATE_MODEL_FIELDS_MUTATION,
79
CREATE_MODEL_MUTATION,
810
CREATE_ROUTE_MUTATION,
@@ -203,4 +205,23 @@ describe("add", () => {
203205
`);
204206
});
205207
});
208+
209+
describe("environments", () => {
210+
it("can add an environment with `add env`", async () => {
211+
nockEditResponse({
212+
operation: CREATE_ENVIRONMENT_MUTATION,
213+
response: { data: { createEnvironment: { slug: "development2", status: EnvironmentStatus.Active } } },
214+
expectVariables: { environment: { slug: "development2", sourceSlug: "development" } },
215+
});
216+
await add.run(testCtx, makeArgs(add.args, "add", "env", "development2"));
217+
});
218+
it("can add an environment with `add environment`", async () => {
219+
nockEditResponse({
220+
operation: CREATE_ENVIRONMENT_MUTATION,
221+
response: { data: { createEnvironment: { slug: "development2", status: EnvironmentStatus.Active } } },
222+
expectVariables: { environment: { slug: "development2", sourceSlug: "development" } },
223+
});
224+
await add.run(testCtx, makeArgs(add.args, "add", "environment", "development2"));
225+
});
226+
});
206227
});

src/__generated__/graphql.ts

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/add.ts

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import terminalLink from "terminal-link";
44
import { getGlobalActions, getModels } from "../services/app/app.js";
55
import {
66
CREATE_ACTION_MUTATION,
7+
CREATE_ENVIRONMENT_MUTATION,
78
CREATE_MODEL_FIELDS_MUTATION,
89
CREATE_MODEL_MUTATION,
910
CREATE_ROUTE_MUTATION,
@@ -49,9 +50,7 @@ export class AddClientError extends GGTError {
4950
export type AddArgs = typeof args;
5051
export type AddArgsResult = ArgsDefinitionResult<AddArgs>;
5152

52-
export const args = {
53-
...SyncJsonArgs,
54-
};
53+
export const args = { ...SyncJsonArgs };
5554

5655
export const usage: Usage = () => {
5756
return sprint`
@@ -80,6 +79,9 @@ export const usage: Usage = () => {
8079
Add a new model 'post' with 2 new 'string' type fields 'title' and 'body':
8180
{cyanBright $ ggt add model post title:string body:string}
8281
82+
Add a new 'boolean' type field 'published' to an existing model
83+
{cyanBright ggt add field post/published:boolean}
84+
8385
Add new action 'publish' to the 'post' model:
8486
{cyanBright ggt add action model/post/publish}
8587
@@ -89,8 +91,8 @@ export const usage: Usage = () => {
8991
Add a new route 'howdy'
9092
{cyanBright ggt add route GET howdy}
9193
92-
Add a new 'boolean' type field 'published' to an existing model
93-
{cyanBright ggt add field post/published:boolean}
94+
Clone the \`development\` environment into a new \`staging\` environment
95+
{cyanBright ggt add environment staging --environment development}
9496
`;
9597
};
9698

@@ -107,12 +109,8 @@ export const run: Run<AddArgs> = async (ctx, args) => {
107109
if (!hashes.inSync) {
108110
await filesync.merge(ctx, {
109111
hashes,
110-
printEnvironmentChangesOptions: {
111-
limit: 5,
112-
},
113-
printLocalChangesOptions: {
114-
limit: 5,
115-
},
112+
printEnvironmentChangesOptions: { limit: 5 },
113+
printLocalChangesOptions: { limit: 5 },
116114
quietly: true,
117115
});
118116
}
@@ -132,6 +130,10 @@ export const run: Run<AddArgs> = async (ctx, args) => {
132130
case "field":
133131
await fieldSubCommand(ctx, { args, filesync });
134132
break;
133+
case "environment":
134+
case "env":
135+
await envSubCommand(ctx, { args, filesync });
136+
break;
135137
default:
136138
println(usage(ctx));
137139
return;
@@ -189,10 +191,7 @@ const modelSubCommand = async (ctx: Context, { args, filesync }: { args: AddArgs
189191
mutation: CREATE_MODEL_MUTATION,
190192
variables: {
191193
path: modelApiIdentifier,
192-
fields: modelFieldsList.map((fields) => ({
193-
name: fields.name,
194-
fieldType: fields.fieldType,
195-
})),
194+
fields: modelFieldsList.map((fields) => ({ name: fields.name, fieldType: fields.fieldType })),
196195
},
197196
})
198197
).createModel;
@@ -206,11 +205,7 @@ const modelSubCommand = async (ctx: Context, { args, filesync }: { args: AddArgs
206205

207206
println({ ensureEmptyLineAbove: true, content: chalk.gray("New model created in environment.") });
208207

209-
await filesync.writeToLocalFilesystem(ctx, {
210-
filesVersion: result.remoteFilesVersion,
211-
files: result.changed,
212-
delete: [],
213-
});
208+
await filesync.writeToLocalFilesystem(ctx, { filesVersion: result.remoteFilesVersion, files: result.changed, delete: [] });
214209

215210
const modelPrintout = terminalLink.isSupported
216211
? terminalLink(
@@ -294,11 +289,7 @@ const actionSubCommand = async (ctx: Context, { args, filesync }: { args: AddArg
294289
})
295290
).createAction;
296291

297-
await filesync.writeToLocalFilesystem(ctx, {
298-
filesVersion: result.remoteFilesVersion,
299-
files: result.changed,
300-
delete: [],
301-
});
292+
await filesync.writeToLocalFilesystem(ctx, { filesVersion: result.remoteFilesVersion, files: result.changed, delete: [] });
302293
} catch (error) {
303294
if (error instanceof ClientError) {
304295
throw new AddClientError(error);
@@ -307,10 +298,7 @@ const actionSubCommand = async (ctx: Context, { args, filesync }: { args: AddArg
307298
}
308299
}
309300

310-
println({
311-
ensureEmptyLineAbove: true,
312-
content: `Action ${chalk.cyanBright(path)} added successfully.`,
313-
});
301+
println({ ensureEmptyLineAbove: true, content: `Action ${chalk.cyanBright(path)} added successfully.` });
314302
};
315303

316304
const routeSubCommand = async (ctx: Context, { args, filesync }: { args: AddArgsResult; filesync: FileSync }): Promise<void> => {
@@ -333,18 +321,10 @@ const routeSubCommand = async (ctx: Context, { args, filesync }: { args: AddArgs
333321
}
334322

335323
try {
336-
const result = (
337-
await syncJson.edit.mutate({
338-
mutation: CREATE_ROUTE_MUTATION,
339-
variables: { method: routeMethod, path: routePath },
340-
})
341-
).createRoute;
324+
const result = (await syncJson.edit.mutate({ mutation: CREATE_ROUTE_MUTATION, variables: { method: routeMethod, path: routePath } }))
325+
.createRoute;
342326

343-
await filesync.writeToLocalFilesystem(ctx, {
344-
filesVersion: result.remoteFilesVersion,
345-
files: result.changed,
346-
delete: [],
347-
});
327+
await filesync.writeToLocalFilesystem(ctx, { filesVersion: result.remoteFilesVersion, files: result.changed, delete: [] });
348328
} catch (error) {
349329
if (error instanceof ClientError) {
350330
throw new AddClientError(error);
@@ -353,10 +333,7 @@ const routeSubCommand = async (ctx: Context, { args, filesync }: { args: AddArgs
353333
}
354334
}
355335

356-
println({
357-
ensureEmptyLineAbove: true,
358-
content: `Route ${chalk.cyanBright(routePath)} added successfully.`,
359-
});
336+
println({ ensureEmptyLineAbove: true, content: `Route ${chalk.cyanBright(routePath)} added successfully.` });
360337
};
361338

362339
const fieldSubCommand = async (ctx: Context, { args, filesync }: { args: AddArgsResult; filesync: FileSync }): Promise<void> => {
@@ -400,18 +377,31 @@ const fieldSubCommand = async (ctx: Context, { args, filesync }: { args: AddArgs
400377
variables: {
401378
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
402379
path: splitPathAndField[0]!,
403-
fields: modelFieldsList.map((field) => ({
404-
name: field.name,
405-
fieldType: field.fieldType,
406-
})),
380+
fields: modelFieldsList.map((field) => ({ name: field.name, fieldType: field.fieldType })),
407381
},
408382
})
409383
).createModelFields;
410384

411-
await filesync.writeToLocalFilesystem(ctx, {
412-
filesVersion: result.remoteFilesVersion,
413-
files: result.changed,
414-
delete: [],
385+
await filesync.writeToLocalFilesystem(ctx, { filesVersion: result.remoteFilesVersion, files: result.changed, delete: [] });
386+
} catch (error) {
387+
if (error instanceof ClientError) {
388+
throw new AddClientError(error);
389+
} else {
390+
throw error;
391+
}
392+
}
393+
394+
println({ ensureEmptyLineAbove: true, content: `Field ${chalk.cyanBright(modelFieldsList[0]?.name)} added successfully.` });
395+
};
396+
397+
const envSubCommand = async (ctx: Context, { args, filesync }: { args: AddArgsResult; filesync: FileSync }): Promise<void> => {
398+
const syncJson = filesync.syncJson;
399+
const newEnvName = args._[1] ?? makeDefaultEnvName();
400+
401+
try {
402+
await syncJson.edit.mutate({
403+
mutation: CREATE_ENVIRONMENT_MUTATION,
404+
variables: { environment: { slug: newEnvName, sourceSlug: syncJson.environment.name } },
415405
});
416406
} catch (error) {
417407
if (error instanceof ClientError) {
@@ -421,8 +411,33 @@ const fieldSubCommand = async (ctx: Context, { args, filesync }: { args: AddArgs
421411
}
422412
}
423413

424-
println({
425-
ensureEmptyLineAbove: true,
426-
content: `Field ${chalk.cyanBright(modelFieldsList[0]?.name)} added successfully.`,
414+
println({ ensureEmptyLineAbove: true, content: `Environment ${chalk.cyanBright(newEnvName)} added successfully.` });
415+
416+
// Try to switch to newly made env
417+
const pullFromNewEnvSyncJson = await SyncJson.load(ctx, {
418+
command: "pull",
419+
args: { _: [], "--app": undefined, "--allow-unknown-directory": undefined, "--allow-different-app": undefined, "--env": newEnvName },
420+
directory: await loadSyncJsonDirectory(process.cwd()),
427421
});
422+
if (pullFromNewEnvSyncJson) {
423+
const filesync = new FileSync(syncJson);
424+
const hashes = await filesync.hashes(ctx);
425+
if (hashes.environmentChangesToPull.size === 0) {
426+
println({ ensureEmptyLineAbove: true, content: "Nothing to pull." });
427+
return;
428+
}
429+
if (hashes.localChangesToPush.size > 0) {
430+
// show them the local changes they will discard
431+
await filesync.print(ctx, { hashes });
432+
}
433+
await filesync.pull(ctx, { hashes, force: true });
434+
}
435+
};
436+
437+
/**
438+
* Creates a default environment name based on the current date and time.
439+
*/
440+
const makeDefaultEnvName = (): string => {
441+
const currentDate = new Date();
442+
return `env-${currentDate.toISOString().slice(0, 10).replace(/-/g, "")}-${currentDate.toLocaleTimeString("en-US", { hour12: false }).replace(/:/g, "")}`;
428443
};

0 commit comments

Comments
 (0)