Comparing version 1.13.0 to 1.14.0
@@ -5,3 +5,3 @@ import { writer } from '../writer.js'; | ||
import { retryOnLock } from '../api.js'; | ||
import { branchIdFromProps } from '../enrichers.js'; | ||
import { branchIdFromProps, fillSingleProject } from '../enrichers.js'; | ||
const BRANCH_FIELDS = ['id', 'name', 'created_at', 'updated_at']; | ||
@@ -19,5 +19,5 @@ export const command = 'branches'; | ||
type: 'string', | ||
demandOption: true, | ||
}, | ||
}) | ||
.middleware(fillSingleProject) | ||
.command('list', 'List branches', (yargs) => yargs, async (args) => await list(args)) | ||
@@ -24,0 +24,0 @@ .command('create', 'Create a branch', (yargs) => yargs.options({ |
import { EndpointType } from '@neondatabase/api-client'; | ||
import { branchIdFromProps } from '../enrichers.js'; | ||
export const command = 'connection-string <branch>'; | ||
import { branchIdFromProps, fillSingleProject } from '../enrichers.js'; | ||
export const command = 'connection-string [branch]'; | ||
export const aliases = ['cs']; | ||
export const describe = 'Get connection string'; | ||
export const builder = (argv) => { | ||
return argv.usage('usage: $0 connection-string <branch> [options]').options({ | ||
return argv | ||
.usage('usage: $0 connection-string [branch] [options]') | ||
.positional('branch', { | ||
describe: 'Branch name or id. If ommited will use the primary branch', | ||
type: 'string', | ||
}) | ||
.options({ | ||
'project.id': { | ||
type: 'string', | ||
describe: 'Project ID', | ||
demandOption: true, | ||
}, | ||
@@ -16,3 +21,2 @@ 'role.name': { | ||
describe: 'Role name', | ||
demandOption: true, | ||
}, | ||
@@ -22,3 +26,2 @@ 'database.name': { | ||
describe: 'Database name', | ||
demandOption: true, | ||
}, | ||
@@ -35,7 +38,9 @@ pooled: { | ||
}, | ||
}); | ||
}) | ||
.middleware(fillSingleProject); | ||
}; | ||
export const handler = async (props) => { | ||
const projectId = props.project.id; | ||
const branchId = await branchIdFromProps(props); | ||
const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(props.project.id, branchId); | ||
const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(projectId, branchId); | ||
const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite); | ||
@@ -45,3 +50,29 @@ if (!endpoint) { | ||
} | ||
const { data: password } = await props.apiClient.getProjectBranchRolePassword(props.project.id, endpoint.branch_id, props.role.name); | ||
const role = props.role?.name || | ||
(await props.apiClient | ||
.listProjectBranchRoles(projectId, branchId) | ||
.then(({ data }) => { | ||
if (data.roles.length === 0) { | ||
throw new Error(`No roles found for the branch: ${branchId}`); | ||
} | ||
if (data.roles.length === 1) { | ||
return data.roles[0].name; | ||
} | ||
throw new Error(`Multiple roles found for the branch, please provide one with the --role.name option: ${data.roles | ||
.map((r) => r.name) | ||
.join(', ')}`); | ||
})); | ||
const database = props.database?.name || | ||
(await props.apiClient | ||
.listProjectBranchDatabases(projectId, branchId) | ||
.then(({ data }) => { | ||
if (data.databases.length === 0) { | ||
throw new Error(`No databases found for the branch: ${branchId}`); | ||
} | ||
if (data.databases.length === 1) { | ||
return data.databases[0].name; | ||
} | ||
throw new Error(`Multiple databases found for the branch, please provide one with the --database.name option: ${data.databases}`); | ||
})); | ||
const { data: password } = await props.apiClient.getProjectBranchRolePassword(props.project.id, endpoint.branch_id, role); | ||
const host = props.pooled | ||
@@ -51,4 +82,4 @@ ? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`) | ||
const connectionString = new URL(`postgres://${host}`); | ||
connectionString.pathname = props.database.name; | ||
connectionString.username = props.role.name; | ||
connectionString.pathname = database; | ||
connectionString.username = role; | ||
connectionString.password = password.password; | ||
@@ -55,0 +86,0 @@ if (props.prisma) { |
@@ -72,2 +72,10 @@ import { describe } from '@jest/globals'; | ||
}); | ||
testCliCommand({ | ||
name: 'connection_string without any args should pass', | ||
args: ['connection-string'], | ||
mockDir: 'single_project', | ||
expected: { | ||
snapshot: true, | ||
}, | ||
}); | ||
}); |
import { retryOnLock } from '../api.js'; | ||
import { branchIdFromProps } from '../enrichers.js'; | ||
import { branchIdFromProps, fillSingleProject } from '../enrichers.js'; | ||
import { databaseCreateRequest } from '../parameters.gen.js'; | ||
@@ -18,3 +18,2 @@ import { commandFailHandler } from '../utils.js'; | ||
type: 'string', | ||
demandOption: true, | ||
}, | ||
@@ -24,5 +23,5 @@ branch: { | ||
type: 'string', | ||
demandOption: true, | ||
}, | ||
}) | ||
.middleware(fillSingleProject) | ||
.command('list', 'List databases', (yargs) => yargs, async (args) => await list(args)) | ||
@@ -29,0 +28,0 @@ .command('create', 'Create a database', (yargs) => yargs.options(databaseCreateRequest), async (args) => await create(args)) |
@@ -0,1 +1,2 @@ | ||
import { fillSingleProject } from '../enrichers.js'; | ||
import { commandFailHandler } from '../utils.js'; | ||
@@ -15,5 +16,5 @@ import { writer } from '../writer.js'; | ||
type: 'string', | ||
demandOption: true, | ||
}, | ||
}) | ||
.middleware(fillSingleProject) | ||
.command('list', 'List operations', (yargs) => yargs, async (args) => await list(args)); | ||
@@ -20,0 +21,0 @@ export const handler = (args) => { |
import { retryOnLock } from '../api.js'; | ||
import { branchIdFromProps } from '../enrichers.js'; | ||
import { branchIdFromProps, fillSingleProject } from '../enrichers.js'; | ||
import { roleCreateRequest } from '../parameters.gen.js'; | ||
@@ -18,3 +18,2 @@ import { commandFailHandler } from '../utils.js'; | ||
type: 'string', | ||
demandOption: true, | ||
}, | ||
@@ -24,5 +23,5 @@ branch: { | ||
type: 'string', | ||
demandOption: true, | ||
}, | ||
}) | ||
.middleware(fillSingleProject) | ||
.command('list', 'List roles', (yargs) => yargs, async (args) => await list(args)) | ||
@@ -29,0 +28,0 @@ .command('create', 'Create a role', (yargs) => yargs.options(roleCreateRequest), async (args) => await create(args)) |
@@ -9,12 +9,41 @@ const HAIKU_REGEX = /^[a-z]+-[a-z]+-\d{6}$/; | ||
if (!branchData) { | ||
throw new Error(`Branch ${branch} not found`); | ||
throw new Error(`Branch ${branch} not found.\nAvailable branches: ${data.branches | ||
.map((b) => b.name) | ||
.join(', ')}`); | ||
} | ||
return branchData.id; | ||
}; | ||
export const branchIdFromProps = async (props) => branchIdResolve({ | ||
branch: 'branch' in props && typeof props.branch === 'string' | ||
export const branchIdFromProps = async (props) => { | ||
const branch = 'branch' in props && typeof props.branch === 'string' | ||
? props.branch | ||
: props.id, | ||
apiClient: props.apiClient, | ||
projectId: props.project.id, | ||
}); | ||
: props.id; | ||
if (branch) { | ||
return await branchIdResolve({ | ||
branch, | ||
apiClient: props.apiClient, | ||
projectId: props.project.id, | ||
}); | ||
} | ||
const { data } = await props.apiClient.listProjectBranches(props.project.id); | ||
const primaryBranch = data.branches.find((b) => b.primary); | ||
if (primaryBranch) { | ||
return primaryBranch.id; | ||
} | ||
throw new Error('No primary branch found'); | ||
}; | ||
export const fillSingleProject = async (props) => { | ||
if (props.project) { | ||
return props; | ||
} | ||
const { data } = await props.apiClient.listProjects({}); | ||
if (data.projects.length === 0) { | ||
throw new Error('No projects found'); | ||
} | ||
if (data.projects.length > 1) { | ||
throw new Error(`Multiple projects found, please provide one with the --project.id option`); | ||
} | ||
return { | ||
...props, | ||
project: { id: data.projects[0].id }, | ||
}; | ||
}; |
export const isCi = () => { | ||
return process.env.CI !== 'false' && Boolean(process.env.CI); | ||
}; | ||
export const isDebug = () => { | ||
return Boolean(process.env.DEBUG); | ||
}; |
@@ -84,3 +84,6 @@ import yargs from 'yargs'; | ||
if (isAxiosError(err)) { | ||
if (err.response?.status === 401) { | ||
if (err.code === 'ECONNABORTED') { | ||
log.error('Request timed out'); | ||
} | ||
else if (err.response?.status === 401) { | ||
log.error('Authentication failed, please run `neonctl auth`'); | ||
@@ -95,2 +98,3 @@ } | ||
} | ||
err.stack && log.degug('Stack: %s', err.stack); | ||
process.exit(1); | ||
@@ -97,0 +101,0 @@ }); |
import { format } from 'node:util'; | ||
import { isDebug } from './env.js'; | ||
export const log = { | ||
degug: (...args) => { | ||
if (isDebug()) { | ||
process.stderr.write(`DEBUG: ${format(...args)}\n`); | ||
} | ||
}, | ||
info: (...args) => { | ||
@@ -4,0 +10,0 @@ process.stderr.write(`INFO: ${format(...args)}\n`); |
@@ -8,3 +8,3 @@ { | ||
"type": "module", | ||
"version": "1.13.0", | ||
"version": "1.14.0", | ||
"description": "CLI tool for NeonDB Cloud management", | ||
@@ -11,0 +11,0 @@ "main": "index.js", |
@@ -7,6 +7,6 @@ /* eslint-disable no-console */ | ||
import { join } from 'node:path'; | ||
const runMockServer = async () => new Promise((resolve) => { | ||
const runMockServer = async (mockDir) => new Promise((resolve) => { | ||
const app = express(); | ||
app.use(express.json()); | ||
app.use('/', emocks(join(process.cwd(), 'mocks'))); | ||
app.use('/', emocks(join(process.cwd(), 'mocks', mockDir))); | ||
const server = app.listen(0); | ||
@@ -18,7 +18,7 @@ server.on('listening', () => { | ||
}); | ||
export const testCliCommand = ({ args, name, expected, }) => { | ||
export const testCliCommand = ({ args, name, expected, mockDir = 'main', }) => { | ||
let server; | ||
describe(name, () => { | ||
beforeAll(async () => { | ||
server = await runMockServer(); | ||
server = await runMockServer(mockDir); | ||
}); | ||
@@ -58,2 +58,5 @@ afterAll(async () => { | ||
try { | ||
if (code !== 0 && error) { | ||
console.error(error); | ||
} | ||
expect(code).toBe(0); | ||
@@ -60,0 +63,0 @@ if (code === 0 && expected) { |
70278
1662