diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..917fdfa --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,5 @@ +{ + "name": "Neon CLI", + "postCreateCommand": "npm install -g bun && bun install && bun run build", + "waitFor": "postCreateCommand" +} diff --git a/mocks/main/projects/GET.js b/mocks/main/projects/GET.js index 065dbf8..177c49b 100644 --- a/mocks/main/projects/GET.js +++ b/mocks/main/projects/GET.js @@ -4,6 +4,53 @@ export default function (req, res) { expect(req.query).toMatchObject({ limit: '100', }); + + if (req.query.org_id) { + expect(['org-2', 'org-3']).toContain(req.query.org_id); + + if (req.query.org_id === 'org-2') { + return res.json({ + projects: [ + { + id: 4, + name: 'Project_4', + created_at: '2019-01-01T00:00:00.000Z', + updated_at: '2019-01-01T00:00:00.000Z', + org_id: 'org-2', + }, + { + id: 5, + name: 'Project_5', + created_at: '2019-01-01T00:00:00.000Z', + updated_at: '2019-01-01T00:00:00.000Z', + org_id: 'org-2', + }, + { + id: 6, + name: 'Project_6', + created_at: '2019-01-01T00:00:00.000Z', + updated_at: '2019-01-01T00:00:00.000Z', + org_id: 'org-2', + }, + ], + }); + } + + if (req.query.org_id === 'org-3') { + return res.json({ + projects: [ + { + id: 7, + name: 'Project_7', + created_at: '2019-01-01T00:00:00.000Z', + updated_at: '2019-01-01T00:00:00.000Z', + org_id: 'org-3', + }, + ], + }); + } + } + res.json({ projects: [ { diff --git a/mocks/main/projects/POST.js b/mocks/main/projects/POST.js index 56ea884..1615e22 100644 --- a/mocks/main/projects/POST.js +++ b/mocks/main/projects/POST.js @@ -30,6 +30,28 @@ export default function (req, res) { }, }); } + + if (req.body.project.org_id) { + expect(req.body.project.org_id).toBe('org-2'); + + return res.json({ + project: { + id: 'new-project-789012', + name: 'test_project', + created_at: '2022-01-01T00:00:00.000Z', + org_id: 'org-2', + }, + connection_uris: [ + { connection_uri: 'postgres://localhost:5432/test_project' }, + ], + branch: { + id: 'br-test-branch-789012', + name: 'test_branch', + created_at: '2022-01-01T00:00:00.000Z', + }, + }); + } + res.send({ project: { id: 'new-project-123456', diff --git a/src/commands/__snapshots__/projects.test.ts.snap b/src/commands/__snapshots__/projects.test.ts.snap index 58e1926..5e80ea4 100644 --- a/src/commands/__snapshots__/projects.test.ts.snap +++ b/src/commands/__snapshots__/projects.test.ts.snap @@ -70,6 +70,17 @@ connection_uris: " `; +exports[`projects > create with org id 1`] = ` +"project: + id: new-project-789012 + name: test_project + created_at: 2022-01-01T00:00:00.000Z + org_id: org-2 +connection_uris: + - connection_uri: postgres://localhost:5432/test_project +" +`; + exports[`projects > delete 1`] = ` "id: test name: test @@ -112,6 +123,25 @@ shared_with_me: " `; +exports[`projects > list with org id 1`] = ` +"- id: 4 + name: Project_4 + created_at: 2019-01-01T00:00:00.000Z + updated_at: 2019-01-01T00:00:00.000Z + org_id: org-2 +- id: 5 + name: Project_5 + created_at: 2019-01-01T00:00:00.000Z + updated_at: 2019-01-01T00:00:00.000Z + org_id: org-2 +- id: 6 + name: Project_6 + created_at: 2019-01-01T00:00:00.000Z + updated_at: 2019-01-01T00:00:00.000Z + org_id: org-2 +" +`; + exports[`projects > update ip allow 1`] = ` "id: test name: test_project diff --git a/src/commands/__snapshots__/set_context.test.ts.snap b/src/commands/__snapshots__/set_context.test.ts.snap index c7f67a8..3f983d3 100644 --- a/src/commands/__snapshots__/set_context.test.ts.snap +++ b/src/commands/__snapshots__/set_context.test.ts.snap @@ -1,6 +1,62 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`set_context > should set the context > get project id overrides context set project 1`] = ` +exports[`set_context > can set the context to project and organization at the same time > set-context 1`] = `""`; + +exports[`set_context > can set the context to project and organization at the same time > set-context 2`] = ` +"{ + "projectId": "test_project", + "orgId": "org-2" +}" +`; + +exports[`set_context > should set the context to organization > create projects selecting organization from the context 1`] = ` +"project: + id: new-project-789012 + name: test_project + created_at: 2022-01-01T00:00:00.000Z + org_id: org-2 +connection_uris: + - connection_uri: postgres://localhost:5432/test_project +" +`; + +exports[`set_context > should set the context to organization > list projects selecting organization from the context 1`] = ` +"- id: 4 + name: Project_4 + created_at: 2019-01-01T00:00:00.000Z + updated_at: 2019-01-01T00:00:00.000Z + org_id: org-2 +- id: 5 + name: Project_5 + created_at: 2019-01-01T00:00:00.000Z + updated_at: 2019-01-01T00:00:00.000Z + org_id: org-2 +- id: 6 + name: Project_6 + created_at: 2019-01-01T00:00:00.000Z + updated_at: 2019-01-01T00:00:00.000Z + org_id: org-2 +" +`; + +exports[`set_context > should set the context to organization > list projects with explicit org id overrides context 1`] = ` +"- id: 7 + name: Project_7 + created_at: 2019-01-01T00:00:00.000Z + updated_at: 2019-01-01T00:00:00.000Z + org_id: org-3 +" +`; + +exports[`set_context > should set the context to organization > set-context 1`] = `""`; + +exports[`set_context > should set the context to organization > set-context 2`] = ` +"{ + "orgId": "org-2" +}" +`; + +exports[`set_context > should set the context to project > get project id overrides context set project 1`] = ` "id: project-id-123 name: project name 123 created_at: 2019-01-01T00:00:00Z @@ -12,7 +68,7 @@ settings: " `; -exports[`set_context > should set the context > list branches selecting project from the context 1`] = ` +exports[`set_context > should set the context to project > list branches selecting project from the context 1`] = ` "- id: br-main-branch-123456 name: main default: true @@ -41,7 +97,7 @@ exports[`set_context > should set the context > list branches selecting project " `; -exports[`set_context > should set the context > set the branchId and projectId is from context 1`] = ` +exports[`set_context > should set the context to project > set the branchId and projectId is from context 1`] = ` "- name: db1 owner_name: user1 - name: db2 @@ -51,11 +107,11 @@ exports[`set_context > should set the context > set the branchId and projectId i " `; -exports[`set_context > should set the context > set the branchId and projectId is from context 2`] = `""`; +exports[`set_context > should set the context to project > set the branchId and projectId is from context 2`] = `""`; -exports[`set_context > should set the context > set-context 1`] = `""`; +exports[`set_context > should set the context to project > set-context 1`] = `""`; -exports[`set_context > should set the context > set-context 2`] = ` +exports[`set_context > should set the context to project > set-context 2`] = ` "{ "projectId": "test" }" diff --git a/src/commands/projects.test.ts b/src/commands/projects.test.ts index 276a98a..d1541a7 100644 --- a/src/commands/projects.test.ts +++ b/src/commands/projects.test.ts @@ -10,10 +10,25 @@ describe('projects', () => { await testCliCommand(['projects', 'list']); }); + test('list with org id', async ({ testCliCommand }) => { + await testCliCommand(['projects', 'list', '--org-id', 'org-2']); + }); + test('create', async ({ testCliCommand }) => { await testCliCommand(['projects', 'create', '--name', 'test_project']); }); + test('create with org id', async ({ testCliCommand }) => { + await testCliCommand([ + 'projects', + 'create', + '--name', + 'test_project', + '--org-id', + 'org-2', + ]); + }); + test('create with database and role', async ({ testCliCommand }) => { await testCliCommand([ 'projects', diff --git a/src/commands/projects.ts b/src/commands/projects.ts index b1b7c47..807eef3 100644 --- a/src/commands/projects.ts +++ b/src/commands/projects.ts @@ -42,7 +42,13 @@ export const builder = (argv: yargs.Argv) => { .command( 'list', 'List projects', - (yargs) => yargs, + (yargs) => + yargs.options({ + 'org-id': { + describe: 'List projects of a given organization', + type: 'string', + }, + }), async (args) => { await list(args as any); }, @@ -60,6 +66,10 @@ export const builder = (argv: yargs.Argv) => { describe: `The region ID. Possible values: ${REGIONS.join(', ')}`, type: 'string', }, + 'org-id': { + describe: "The project's organization ID", + type: 'string', + }, psql: { type: 'boolean', describe: 'Connect to a new project via psql', @@ -146,7 +156,7 @@ export const handler = (args: yargs.Argv) => { return args; }; -const list = async (props: CommonProps) => { +const list = async (props: CommonProps & { orgId?: string }) => { const getList = async ( fn: | typeof props.apiClient.listProjects @@ -158,6 +168,7 @@ const list = async (props: CommonProps) => { while (!end) { const { data } = await fn({ limit: PROJECTS_LIST_LIMIT, + org_id: props.orgId, cursor, }); result.push(...data.projects); @@ -175,21 +186,25 @@ const list = async (props: CommonProps) => { return result; }; - const [ownedProjects, sharedProjects] = await Promise.all([ - getList(props.apiClient.listProjects), - getList(props.apiClient.listSharedProjects), - ]); + const ownedProjects = getList(props.apiClient.listProjects); + const sharedProjects = props.orgId + ? undefined + : getList(props.apiClient.listSharedProjects); const out = writer(props); - out.write(ownedProjects, { + out.write(await ownedProjects, { fields: PROJECT_FIELDS, title: 'Projects', }); - out.write(sharedProjects, { - fields: PROJECT_FIELDS, - title: 'Shared with me', - }); + + if (sharedProjects) { + out.write(await sharedProjects, { + fields: PROJECT_FIELDS, + title: 'Shared with me', + }); + } + out.end(); }; @@ -198,6 +213,7 @@ const create = async ( name?: string; regionId?: string; cu?: string; + orgId?: string; database?: string; role?: string; psql: boolean; @@ -212,6 +228,9 @@ const create = async ( if (props.regionId) { project.region_id = props.regionId; } + if (props.orgId) { + project.org_id = props.orgId; + } project.branch = {}; if (props.database) { project.branch.database_name = props.database; diff --git a/src/commands/set_context.test.ts b/src/commands/set_context.test.ts index df78b09..767aaa5 100644 --- a/src/commands/set_context.test.ts +++ b/src/commands/set_context.test.ts @@ -36,7 +36,7 @@ const test = originalTest.extend<{ }); describe('set_context', () => { - describe('should set the context', () => { + describe('should set the context to project', () => { test('set-context', async ({ testCliCommand, readFile }) => { await testCliCommand([ 'set-context', @@ -124,4 +124,81 @@ describe('set_context', () => { ); }); }); + + describe('should set the context to organization', () => { + test('set-context', async ({ testCliCommand, readFile }) => { + await testCliCommand([ + 'set-context', + '--org-id', + 'org-2', + '--context-file', + CONTEXT_FILE, + ]); + expect(readFile(CONTEXT_FILE)).toMatchSnapshot(); + }); + + test('list projects selecting organization from the context', async ({ + testCliCommand, + writeFile, + }) => { + writeFile(CONTEXT_FILE, { + orgId: 'org-2', + }); + await testCliCommand([ + 'projects', + 'list', + '--context-file', + CONTEXT_FILE, + ]); + }); + + test('list projects with explicit org id overrides context', async ({ + testCliCommand, + writeFile, + }) => { + writeFile(CONTEXT_FILE, { + orgId: 'org-2', + }); + await testCliCommand([ + 'project', + 'list', + '--org-id', + 'org-3', + '--context-file', + CONTEXT_FILE, + ]); + }); + + test('create projects selecting organization from the context', async ({ + testCliCommand, + writeFile, + }) => { + writeFile(CONTEXT_FILE, { + orgId: 'org-2', + }); + await testCliCommand([ + 'projects', + 'create', + '--name', + 'test_project', + '--context-file', + CONTEXT_FILE, + ]); + }); + }); + + describe('can set the context to project and organization at the same time', () => { + test('set-context', async ({ testCliCommand, readFile }) => { + await testCliCommand([ + 'set-context', + '--project-id', + 'test_project', + '--org-id', + 'org-2', + '--context-file', + CONTEXT_FILE, + ]); + expect(readFile(CONTEXT_FILE)).toMatchSnapshot(); + }); + }); }); diff --git a/src/commands/set_context.ts b/src/commands/set_context.ts index 4daece3..0fdf964 100644 --- a/src/commands/set_context.ts +++ b/src/commands/set_context.ts @@ -1,6 +1,11 @@ import yargs from 'yargs'; import { Context, updateContextFile } from '../context.js'; -import { BranchScopeProps } from '../types.js'; +import { CommonProps } from '../types.js'; + +type SetContextProps = { + projectId?: string; + orgId?: string; +}; export const command = 'set-context'; export const describe = 'Set the current context'; @@ -10,11 +15,16 @@ export const builder = (argv: yargs.Argv) => describe: 'Project ID', type: 'string', }, + 'org-id': { + describe: 'Organization ID', + type: 'string', + }, }); -export const handler = (props: BranchScopeProps) => { +export const handler = (props: CommonProps & SetContextProps) => { const context: Context = { projectId: props.projectId, + orgId: props.orgId, }; updateContextFile(props.contextFile, context); }; diff --git a/src/context.ts b/src/context.ts index e57ffa8..0cbb0b9 100644 --- a/src/context.ts +++ b/src/context.ts @@ -4,6 +4,7 @@ import { normalize, resolve } from 'node:path'; import yargs from 'yargs'; export type Context = { + orgId?: string; projectId?: string; branchId?: string; }; @@ -48,6 +49,9 @@ export const enrichFromContext = ( return; } const context = readContextFile(args.contextFile); + if (!args.orgId) { + args.orgId = context.orgId; + } if (!args.projectId) { args.projectId = context.projectId; }