diff --git a/src/auth-request.ts b/src/auth-request.ts index 1967f20..85aa628 100644 --- a/src/auth-request.ts +++ b/src/auth-request.ts @@ -12,7 +12,7 @@ interface Handlers { onMessage: (event: MessageEvent) => void } -export function readEventSource(url: string, discoConfig: DiscoConfig, handlers: Handlers) { +export function readEventSource(url: string, discoConfig: DiscoConfig, handlers: Handlers): Promise { const params: EventSourceInitDict = { headers: { Accept: 'text/event-stream', @@ -27,8 +27,11 @@ export function readEventSource(url: string, discoConfig: DiscoConfig, handlers: // 'output' is our way of saying that we're sending a message es.addEventListener('output', handlers.onMessage) // sending 'end' is our way of signaling that we want to close the connection - es.addEventListener('end', () => { - es.close() + return new Promise((resolve) => { + es.addEventListener('end', () => { + es.close() + resolve() + }) }) } @@ -46,7 +49,7 @@ export function request({ discoConfig: DiscoConfig body?: unknown expectedStatuses?: number[] - extraHeaders?: Record, + extraHeaders?: Record bodyStream?: Readable }) { const params: RequestInit = { diff --git a/src/commands/postgres/create.ts b/src/commands/postgres/create.ts new file mode 100644 index 0000000..1164e4e --- /dev/null +++ b/src/commands/postgres/create.ts @@ -0,0 +1,138 @@ +import {Command, Flags} from '@oclif/core' +import {getDisco} from '../../config.js' +import {request, readEventSource} from '../../auth-request.js' + +type PostgresInstance = { + name: string + version: string + created: string +} + +type PostgresDatabase = { + created: string + name: string +} + +export default class PostgresCreate extends Command { + static description = 'create a database for a project, ensuring addon and instance are installed' + + static examples = ['<%= config.bin %> <%= command.id %>'] + + static flags = { + disco: Flags.string({required: false}), + project: Flags.string({required: true}), + 'env-var': Flags.string({required: true, default: 'DATABASE_URL'}), + } + + public async run(): Promise { + const {flags} = await this.parse(PostgresCreate) + const discoConfig = getDisco(flags.disco || null) + let instances: PostgresInstance[] | undefined + let instanceName: string | undefined + { + // get list of instances + const url = `https://${discoConfig.host}/api/projects/postgres-addon/cgi/endpoints/instances` + const res = await request({ + method: 'GET', + url, + discoConfig, + expectedStatuses: [200, 404], + extraHeaders: { + 'X-Disco-Include-API-Key': 'true', + }, + }) + if (res.status === 200) { + const respBody = (await res.json()) as {instances: PostgresInstance[]} + instances = respBody.instances + } else { + // res.status == 404 + this.log('Postgres addon not installed. Installing.') + const url = `https://${discoConfig.host}/api/projects` + const project = 'postgres-addon' + const body = { + name: project, + githubRepo: 'letsdiscodev/disco-addon-postgres', + } + + const res = await request({method: 'POST', url, discoConfig, body, expectedStatuses: [201]}) + const data = (await res.json()) as any + if (data.deployment) { + const url = `https://${discoConfig.host}/api/projects/${project}/deployments/${data.deployment.number}/output` + await readEventSource(url, discoConfig, { + onMessage(event: MessageEvent) { + process.stdout.write(JSON.parse(event.data).text) + }, + }) + } + + instances = [] + } + } + + if (instances.length === 0) { + this.log('No Postgres instance yet. Adding one.') + const url = `https://${discoConfig.host}/api/projects/postgres-addon/cgi/endpoints/instances` + const res = await request({ + method: 'POST', + url, + discoConfig, + expectedStatuses: [201], + extraHeaders: { + 'X-Disco-Include-API-Key': 'true', + }, + }) + const respBody = (await res.json()) as { + instance: {name: string} + project: {name: string} + deployment: {number: number} + } + this.log(`Added instance ${respBody.instance.name}.`) + const deploymentUrl = `https://${discoConfig.host}/api/projects/${respBody.project.name}/deployments/${respBody.deployment.number}/output` + + await readEventSource(deploymentUrl, discoConfig, { + onMessage(event: MessageEvent) { + process.stdout.write(JSON.parse(event.data).text) + }, + }) + instanceName = respBody.instance.name + } else { + instances.sort((a, b) => new Date(a.created).valueOf() - new Date(b.created).valueOf()) + instanceName = instances.at(-1)?.name + this.log(`Using Postgres instance ${instanceName}.`) + } + + let databaseName: string | undefined + { + this.log('Creating database.') + const url = `https://${discoConfig.host}/api/projects/postgres-addon/cgi/endpoints/instances/${instanceName}/databases` + const res = await request({ + method: 'POST', + url, + discoConfig, + expectedStatuses: [201], + extraHeaders: { + 'X-Disco-Include-API-Key': 'true', + }, + }) + const respBody = (await res.json()) as {database: PostgresDatabase} + this.log(respBody.database.name) + databaseName = respBody.database.name + } + + { + this.log('Attaching database to project.') + const url = `https://${discoConfig.host}/api/projects/postgres-addon/cgi/endpoints/instances/${instanceName}/databases/${databaseName}/attach` + const reqBody = {envVar: flags['env-var'], project: flags.project} + await request({ + method: 'POST', + url, + discoConfig, + body: reqBody, + expectedStatuses: [200], + extraHeaders: { + 'X-Disco-Include-API-Key': 'true', + }, + }) + } + } +} diff --git a/src/commands/postgres/instances/add.ts b/src/commands/postgres/instances/add.ts index 77e1e7a..aee48e8 100644 --- a/src/commands/postgres/instances/add.ts +++ b/src/commands/postgres/instances/add.ts @@ -24,8 +24,8 @@ export default class PostgresInstancesAdd extends Command { 'X-Disco-Include-API-Key': 'true', }, }) - const respBody = (await res.json()) as {project: {name: string}; deployment: {number: number}} - this.log(`Added instance ${respBody.project.name}`) + const respBody = (await res.json()) as {instance: {name: string}, project: {name: string}; deployment: {number: number}} + this.log(`Added instance ${respBody.instance.name}`) const deploymentUrl = `https://${discoConfig.host}/api/projects/${respBody.project.name}/deployments/${respBody.deployment.number}/output` readEventSource(deploymentUrl, discoConfig, {