From 18109cf7e94dd2cd34a4545828a37704e0574cd1 Mon Sep 17 00:00:00 2001 From: fauna-chase Date: Tue, 28 Nov 2023 11:08:57 -0600 Subject: [PATCH] improve shell error handling FE-4795 see above jira for more details 1. shell will not exit on errors 2. shell will correctly reconnect to core in the presence of an intermittent connection error --- src/commands/eval.js | 30 ++++----- src/commands/shell.js | 19 +++--- src/lib/environment-factory.ts | 10 +-- src/lib/fauna-client.ts | 107 ++++++++++++--------------------- 4 files changed, 69 insertions(+), 97 deletions(-) diff --git a/src/commands/eval.js b/src/commands/eval.js index eecd5022..a953070f 100644 --- a/src/commands/eval.js +++ b/src/commands/eval.js @@ -171,25 +171,21 @@ class EvalCommand extends FaunaCommand { } async performV10Query(client, fqlQuery, outputFile, flags) { - try { - let format; - if (flags.format === "shell") { - format = "decorated"; - } else if (flags.format === "json-tagged") { - format = "tagged"; - } else { - format = "simple"; - } + let format; + if (flags.format === "shell") { + format = "decorated"; + } else if (flags.format === "json-tagged") { + format = "tagged"; + } else { + format = "simple"; + } - const res = await client.query(fqlQuery, { - format, - typecheck: flags.typecheck, - }); + const res = await client.query(fqlQuery, { + format, + typecheck: flags.typecheck, + }); - return await this.writeFormattedOutputV10(outputFile, res, flags.format); - } catch (error) { - this.error(`${error.code}\n\n${error.queryInfo.summary}`); - } + return this.writeFormattedOutputV10(outputFile, res, flags.format); } async performV4Query(client, fqlQuery, outputFile, flags) { diff --git a/src/commands/shell.js b/src/commands/shell.js index f3c64d4d..802932ac 100644 --- a/src/commands/shell.js +++ b/src/commands/shell.js @@ -83,17 +83,22 @@ class ShellCommand extends EvalCommand { if (cmd.trim() === "") return cb(); if (this.flags.version === "10") { - const res = await this.performV10Query( - this.connection.client, - cmd, - null, - { + let res; + try { + res = await this.performV10Query(this.connection.client, cmd, null, { format: "shell", version: "10", typecheck: this.flags.typecheck, + }); + } catch (err) { + let errString = ""; + if (err.code) { + errString = errString.concat(`${err.code}\n`); } - ); - + errString = errString.concat(err.message); + console.error(errString); + return cb(null); + } console.log(res); return cb(null); diff --git a/src/lib/environment-factory.ts b/src/lib/environment-factory.ts index 86be3853..6b743799 100644 --- a/src/lib/environment-factory.ts +++ b/src/lib/environment-factory.ts @@ -1,7 +1,7 @@ import { confirm, input } from "@inquirer/prompts"; import { Command, ux } from "@oclif/core"; import { Endpoint, ShellConfig } from "./config"; -import FaunaClient, { QuerySuccess } from "./fauna-client"; +import FaunaClient, { QueryFailure, QuerySuccess } from "./fauna-client"; import { searchSelect } from "./search-select"; export interface AddEnvironmentParams { @@ -112,7 +112,9 @@ export class EnvironmentFactory { database: databaseName, }; this.config.saveProjectConfig(); - console.log(`Saved environment ${name} to ${this.config.projectConfigFile()}`); + console.log( + `Saved environment ${name} to ${this.config.projectConfigFile()}` + ); } promptDatabasePath = async (endpoint: Endpoint): Promise => { @@ -124,7 +126,7 @@ export class EnvironmentFactory { const res = await client.query("0"); if (res.status !== 200) { - this.cmd.error(`${res.body.error.code}`); + this.cmd.error(`${(res as QueryFailure).body.error.code}`); } const databasePaths = await this.getDatabasePaths(client); @@ -199,7 +201,7 @@ export class EnvironmentFactory { } ); if (databases.status !== 200) { - this.cmd.error(`Error: ${databases.body.error.code}`); + this.cmd.error(`Error: ${(databases as QueryFailure).body.error.code}`); } const dbs = (databases as QuerySuccess).body.data; diff --git a/src/lib/fauna-client.ts b/src/lib/fauna-client.ts index a7bc27e7..8d7b7429 100644 --- a/src/lib/fauna-client.ts +++ b/src/lib/fauna-client.ts @@ -1,27 +1,18 @@ -import { connect, constants } from "http2"; - -// Copied from the fauna-js driver: -// https://github.com/fauna/fauna-js/blob/main/src/http-client/node-http2-client.ts +import fetch from "node-fetch"; export type QueryResponse = QuerySuccess | QueryFailure; -export type QueryInfo = { - headers: any; - body: { - summary: string; - }; -}; - -export type QuerySuccess = QueryInfo & { +export type QuerySuccess = { status: 200; body: { data: T; }; }; -export type QueryFailure = QueryInfo & { - status: 400; +export type QueryFailure = { + status: number; body: { + summary?: string; error: { code: string; message?: string; @@ -30,16 +21,12 @@ export type QueryFailure = QueryInfo & { }; export default class FaunaClient { - session: any; + endpoint: string; secret: string; timeout?: number; constructor(opts: { endpoint: string; secret: string; timeout?: number }) { - this.session = connect(opts.endpoint, { - peerMaxConcurrentStreams: 50, - }) - .once("error", () => this.close()) - .once("goaway", () => this.close()); + this.endpoint = opts.endpoint; this.secret = opts.secret; this.timeout = opts.timeout; } @@ -57,57 +44,39 @@ export default class FaunaClient { typecheck: opts?.typecheck ?? undefined, secret: opts?.secret ?? this.secret, }; - return new Promise((resolvePromise, rejectPromise) => { - let req: any; - const onResponse = (http2ResponseHeaders: any) => { - const status = http2ResponseHeaders[constants.HTTP2_HEADER_STATUS]; - let responseData = ""; + const url = new URL(this.endpoint); + url.pathname = "/query/1"; + const res = await fetch(url, { + method: "POST", + headers: { + authorization: `Bearer ${secret ?? this.secret}`, + "x-fauna-source": "Fauna Shell", + ...(typecheck !== undefined && { "x-typecheck": typecheck.toString() }), + ...(format !== undefined && { "x-format": format }), + }, + body: JSON.stringify({ query }), + }); - req.on("data", (chunk: any) => { - responseData += chunk; - }); + const json = await res.json(); - req.on("end", () => { - resolvePromise({ - status, - body: JSON.parse(responseData), - headers: http2ResponseHeaders, - }); - }); + if (res.status === 200 || res.status === 201) { + return { + status: 200, + body: { + data: json.data as T, + }, }; - - try { - const httpRequestHeaders = { - Authorization: `Bearer ${secret}`, - "x-format": format, - "X-Fauna-Source": "Fauna Shell", - [constants.HTTP2_HEADER_PATH]: "/query/1", - [constants.HTTP2_HEADER_METHOD]: "POST", - ...((typecheck && { "x-typecheck": typecheck }) ?? {}), - ...((this.timeout && { "x-query-timeout-ms": this.timeout }) ?? {}), - }; - - req = this.session - .request(httpRequestHeaders) - .setEncoding("utf8") - .on("error", (error: any) => rejectPromise(error)) - .on("response", onResponse); - - req.write(JSON.stringify({ query }), "utf8"); - - // req.setTimeout must be called before req.end() - req.setTimeout((this.timeout ?? 0) + 5000, () => { - req.destroy(new Error(`Client timeout`)); - }); - - req.end(); - } catch (error) { - rejectPromise(error); - } - }); - } - - async close() { - this.session.close(); + } else { + return { + status: res.status, + body: { + summary: json.summary, + error: { + code: json.error?.code, + message: json.error?.message, + }, + }, + }; + } } }