Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve link command #10257

Merged
merged 1 commit into from
Feb 29, 2024
Merged
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
218 changes: 184 additions & 34 deletions packages/db/src/core/cli/commands/link/index.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,60 @@
import { mkdir, writeFile } from 'node:fs/promises';
import type { AstroConfig } from 'astro';
import { slug } from 'github-slugger';
import { bgRed, cyan } from 'kleur/colors';
import { mkdir, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { basename } from 'node:path';
import ora from 'ora';
import prompts from 'prompts';
import type { Arguments } from 'yargs-parser';
import { MISSING_SESSION_ID_ERROR } from '../../../errors.js';
import { PROJECT_ID_FILE, getSessionIdFromFile } from '../../../tokens.js';
import { getAstroStudioUrl } from '../../../utils.js';

export async function cmd({ flags }: { config: AstroConfig; flags: Arguments }) {
const linkUrl = new URL(getAstroStudioUrl() + '/auth/cli/link');
export async function cmd({}: { config: AstroConfig; flags: Arguments }) {
const sessionToken = await getSessionIdFromFile();
if (!sessionToken) {
console.error(MISSING_SESSION_ID_ERROR);
process.exit(1);
}
let body = { id: flags._[4] } as {
id?: string;
projectIdName?: string;
workspaceIdName?: string;
};
if (!body.id) {
const workspaceIdName = await promptWorkspaceName();
const projectIdName = await promptProjectName();
body = { projectIdName, workspaceIdName };
const getWorkspaceIdAsync = getWorkspaceId();
await promptBegin();
const isLinkExisting = await promptLinkExisting();
if (isLinkExisting) {
const workspaceId = await getWorkspaceIdAsync;
const existingProjectData = await promptExistingProjectName({workspaceId});
return await linkProject(existingProjectData.id);
}

const isLinkNew = await promptLinkNew();
if (isLinkNew) {
const workspaceId = await getWorkspaceIdAsync;
const newProjectName = await promptNewProjectName();
const newProjectRegion = await promptNewProjectRegion();
const spinner = ora('Creating new project...').start();
const newProjectData = await createNewProject({workspaceId, name: newProjectName, region: newProjectRegion});
// TODO(fks): Actually listen for project creation before continuing
// This is just a dumb spinner that roughly matches database creation time.
await new Promise((r) => setTimeout(r, 4000));
spinner.succeed('Project created!');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"First make it work, then make it good."
~ Confucius, probably

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this is addressed, it would be great to provide a link to your project dashboard!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh nice, that's a great call. Just refactored the code a bit to make this even easier in the future

return await linkProject(newProjectData.id);
}
}

async function linkProject(id: string) {
await mkdir(new URL('.', PROJECT_ID_FILE), { recursive: true });
await writeFile(PROJECT_ID_FILE, `${id}`);
console.info('Project linked.');
}

async function getWorkspaceId(): Promise<string> {
const linkUrl = new URL(getAstroStudioUrl() + '/api/cli/workspaces.list');
const response = await fetch(linkUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${await getSessionIdFromFile()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
// Unauthorized
Expand All @@ -42,38 +66,164 @@ export async function cmd({ flags }: { config: AstroConfig; flags: Arguments })
);
process.exit(1);
}
console.error(`Failed to fetch user workspace: ${response.status} ${response.statusText}`);
process.exit(1);
}
const { data, success } = await response.json() as {success: false, data: unknown} | {success: true, data: {id: string}[]};
if (!success) {
console.error(`Failed to fetch user's workspace.`);
process.exit(1);
}
return data[0].id;
}

console.error(`Failed to link project: ${response.status} ${response.statusText}`);
export async function createNewProject({workspaceId, name, region}: {workspaceId: string; name: string, region: string}) {
const linkUrl = new URL(getAstroStudioUrl() + '/api/cli/projects.create');
const response = await fetch(linkUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${await getSessionIdFromFile()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ workspaceId, name, region }),
});
if (!response.ok) {
// Unauthorized
if (response.status === 401) {
console.error(
`${bgRed('Unauthorized')}\n\n Are you logged in?\n Run ${cyan(
'astro db login'
)} to authenticate and then try linking again.\n\n`
);
process.exit(1);
}
console.error(`Failed to create project: ${response.status} ${response.statusText}`);
process.exit(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we standardize to the { success: boolean, value: JSON } format for reading errors? I worry statusText could log errors that aren't human-readable

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah good catch, I think that already is the response format and this is just checking for the case where our entire API fails and you just get back an unhandled 500. But, we should also have proper handling for the handled error case.

}
const { data } = await response.json();
await mkdir(new URL('.', PROJECT_ID_FILE), { recursive: true });
await writeFile(PROJECT_ID_FILE, `${data.id}`);
console.info('Project linked.');
const { data, success } = await response.json() as {success: false, data: unknown} | {success: true, data: {id: string; idName: string}};
if (!success) {
console.error(`Failed to create project.`);
process.exit(1);
}
return {id: data.id, idName: data.idName};
}

export async function promptProjectName(defaultName?: string): Promise<string> {
const { projectName } = await prompts({
type: 'text',
name: 'projectName',
message: 'Project ID',
initial: defaultName,
export async function promptExistingProjectName({workspaceId}: {workspaceId: string}) {
const linkUrl = new URL(getAstroStudioUrl() + '/api/cli/projects.list');
const response = await fetch(linkUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${await getSessionIdFromFile()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({workspaceId}),
});
if (!response.ok) {
// Unauthorized
if (response.status === 401) {
console.error(
`${bgRed('Unauthorized')}\n\n Are you logged in?\n Run ${cyan(
'astro db login'
)} to authenticate and then try linking again.\n\n`
);
process.exit(1);
}
console.error(`Failed to fetch projects: ${response.status} ${response.statusText}`);
process.exit(1);
}
const { data, success } = await response.json() as {success: false, data: unknown} | {success: true, data: {id: string; idName: string}[]};
if (!success) {
console.error(`Failed to fetch projects.`);
process.exit(1);
}
const { projectId } = await prompts({
type: 'autocomplete',
name: 'projectId',
message: 'What is your project name?',
limit: 5,
choices: data.map((p: any) => ({title: p.name, value: p.id})),
});
if (typeof projectName !== 'string') {
if (typeof projectId !== 'string') {
console.log('Canceled.')
process.exit(0);
}
return projectName;
const selectedProjectData = data.find((p: any) => p.id === projectId)!;
return selectedProjectData;
}

export async function promptBegin(): Promise<void> {
// Get the current working directory relative to the user's home directory
const prettyCwd = process.cwd().replace(homedir(), '~');

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please work on windows please work on windows please work on

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should... right? homedir() is platform agnostic so I think this should work. We'll bug @itsMapleLeaf after merging to test :)

// prompt
const { begin } = await prompts({
type: 'confirm',
name: 'begin',
message: `Link "${prettyCwd}" with Astro Studio?`,
initial: true,
});
if (!begin) {
console.log('Canceled.')
process.exit(0);
};
}

export async function promptLinkExisting(): Promise<boolean> {
// prompt
const { linkExisting } = await prompts({
type: 'confirm',
name: 'linkExisting',
message: `Link with an existing project in Astro Studio?`,
initial: true,
});
return !!linkExisting;
}

export async function promptLinkNew(): Promise<boolean> {
// prompt
const { linkNew } = await prompts({
type: 'confirm',
name: 'linkNew',
message: `Create a new project in Astro Studio?`,
initial: true,
});
if (!linkNew) {
console.log('Canceled.')
process.exit(0);
};
return true;
}

export async function promptWorkspaceName(defaultName?: string): Promise<string> {
const { workspaceName } = await prompts({

export async function promptNewProjectName(): Promise<string> {
const { newProjectName } = await prompts({
type: 'text',
name: 'workspaceName',
message: 'Workspace ID',
initial: defaultName,
name: 'newProjectName',
message: `What is your new project's name?`,
initial: basename(process.cwd()),
format: (val) => slug(val),
});
if (typeof workspaceName !== 'string') {
if (!newProjectName) {
console.log('Canceled.')
process.exit(0);
}
return workspaceName;
};
return newProjectName;
}

export async function promptNewProjectRegion(): Promise<string> {
const { newProjectRegion } = await prompts({
type: 'select',
name: 'newProjectRegion',
message: `Where should your new database live?`,
choices: [
{title: 'North America (East)', value: 'NorthAmericaEast'},
{title: 'North America (West)', value: 'NorthAmericaWest'}
],
initial: 0,
});
if (!newProjectRegion) {
console.log('Canceled.')
process.exit(0);
};
return newProjectRegion;
}
Loading