Skip to content

Commit

Permalink
[FE-2751][FE-2752] Basic documentation support via TSDoc, linting, an…
Browse files Browse the repository at this point in the history
…d intial modularlization of the code. Basic types for the wire protocol. (#2)
  • Loading branch information
cleve-fauna authored Oct 12, 2022
1 parent ba50619 commit df33094
Show file tree
Hide file tree
Showing 10 changed files with 901 additions and 33 deletions.
21 changes: 21 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
overrides: [],
parser: "@typescript-eslint/parser",
parserOptions: {
project: "./tsconfig.json",
tsConfigRootDir: __dirname,
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint", "eslint-plugin-tsdoc"],
rules: {
"@typescript-eslint/no-explicit-any": ["off"],
"tsdoc/syntax": "error",
},
};
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint
yarn pretty-quick --staged
23 changes: 23 additions & 0 deletions __tests__/functional/client-configuration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Client } from "../../src/client";
import { endpoints } from "../../src/client-configuration";

describe("endpoints", () => {
it("is extensible", async () => {
endpoints["my-alternative-port"] = new URL("http://localhost:7443");
expect(endpoints).toEqual({
cloud: new URL("https://db.fauna.com"),
local: new URL("http://localhost:8443"),
"my-alternative-port": new URL("http://localhost:7443"),
});
const client = new Client({
endpoint: endpoints["my-alternative-port"],
maxConns: 5,
secret: "secret",
queryTimeoutMillis: 60,
});
expect(client.client.defaults.baseURL).toEqual("http://localhost:7443/");
const result = await client.query<number>({ query: '"taco".length' });
expect(result.txn_time).not.toBeUndefined();
expect(result).toEqual({ data: 4, txn_time: result.txn_time });
});
});
23 changes: 0 additions & 23 deletions __tests__/functional/poc.test.ts

This file was deleted.

28 changes: 28 additions & 0 deletions __tests__/integration/query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Client } from "../../src/client";
import { endpoints } from "../../src/client-configuration";
import { QueryError } from "../../src/wire-protocol";
import { env } from "process";

describe("query", () => {
const client = new Client({
endpoint: env["endpoint"] ? new URL(env["endpoint"]) : endpoints.local,
maxConns: 5,
secret: env["secret"] || "secret",
queryTimeoutMillis: 60,
});

it("Can query an FQL-x endpoint", async () => {
const result = await client.query<number>({ query: '"taco".length' });
expect(result.txn_time).not.toBeUndefined();
expect(result).toEqual({ data: 4, txn_time: result.txn_time });
});

it("Throws an error if the query is invalid", async () => {
expect.assertions(1);
try {
await client.query<number>({ query: '"taco".length;' });
} catch (e) {
expect(e).toEqual(new QueryError("Query failed."));
}
});
});
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
"author": "Fauna",
"private": true,
"dependencies": {
"agentkeepalive": "^4.2.1",
"axios": "^1.1.2"
},
"devDependencies": {
"@tsconfig/node16-strictest": "^1.0.4",
"@types/jest": "^29.1.2",
"@types/node": "^18.8.3",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"eslint": "^8.25.0",
"eslint-plugin-tsdoc": "^0.2.17",
"husky": "^8.0.1",
"jest": "^29.1.2",
"prettier": "^2.7.1",
Expand All @@ -23,7 +28,9 @@
"scripts": {
"prepare": "husky install",
"build": "tsc --project ./",
"lint": "eslint -f unix \"src/**/*.{ts,tsx}\"",
"fauna-local": "docker start faunadb-fql-x || docker run --rm -d --name faunadb-fql-x -p 8443:8443 -p 8084:8084 -v $PWD/fauna-local-config.yml:/fauna/etc/fauna-local-config.yml gcr.io/faunadb-cloud/faunadb/enterprise --config /fauna/etc/fauna-local-config.yml",
"fauna-local-alt-port": "docker start faunadb-fql-x-alt-port || docker run --rm -d --name faunadb-fql-x-alt-port -p 7443:8443 -p 7084:8084 -v $PWD/fauna-local-config.yml:/fauna/etc/fauna-local-config.yml gcr.io/faunadb-cloud/faunadb/enterprise --config /fauna/etc/fauna-local-config.yml",
"test": "jest"
}
}
56 changes: 56 additions & 0 deletions src/client-configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Configuration for a client.
*/
export interface ClientConfiguration {
/**
* The {@link URL} of Fauna to call. See {@link endpoints} for some default options.
*/
endpoint: URL;
/**
* The maximum number of connections to a make to Fauna.
*/
maxConns: number;
/**
* A secret for your Fauna DB, used to authorize your queries.
* @see https://docs.fauna.com/fauna/current/security/keys
*/
secret: string;
/**
* The timeout of the query, in milliseconds. This controls the maximum amount of
* time Fauna will execute your query before marking it failed.
*/
queryTimeoutMillis: number;
}

/**
* An extensible interface for a set of Fauna endpoints.
* @remarks Leverage the `[key: string]: URL;` field to extend to other endpoints.
*/
export interface Endpoints {
/** Fauna's cloud endpoint. */
cloud: URL;
/**
* An endpoint for interacting with local instance of Fauna (e.g. one running in a local docker container).
*/
local: URL;
/**
* Any other endpoint you want your client to support. For example, if you run all requests through a proxy
* configure it here. Most clients will not need to leverage this ability.
*/
[key: string]: URL;
}

/**
* A extensible set of endpoints for calling Fauna.
* @remarks Most clients will will not need to extend this set.
* @example
* ## To Extend
* ```typescript
* // add to the endpoints constant
* endpoints.myProxyEndpoint = new URL("https://my.proxy.url");
* ```
*/
export const endpoints: Endpoints = {
cloud: new URL("https://db.fauna.com"),
local: new URL("http://localhost:8443"),
};
79 changes: 79 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import axios, { type Axios } from "axios";
import Agent, { HttpsAgent } from "agentkeepalive";
import type { ClientConfiguration } from "./client-configuration";
import {
QueryError,
type QueryRequest,
type QueryResponse,
} from "./wire-protocol";

/**
* Client for calling Fauna.
*/
export class Client {
/** The {@link ClientConfiguration} */
readonly clientConfiguration: ClientConfiguration;
/** The underlying {@link Axios} client. */
readonly client: Axios;

/**
* Constructs a new {@link Client}.
* @param clientConfiguration - the {@link ClientConfiguration} to apply.
* @example
* ```typescript
* const myClient = new Client(
* {
* endpoint: endpoints.classic,
* secret: "foo",
* queryTimeoutMs: 60_000,
* }
* );
* ```
*/
constructor(clientConfiguration: ClientConfiguration) {
this.clientConfiguration = clientConfiguration;
// ensure the network timeout > ClientConfiguration.queryTimeoutMillis so we don't
// terminate connections on active queries.
const timeout = this.clientConfiguration.queryTimeoutMillis + 10_000;
const agentSettings = {
maxSockets: this.clientConfiguration.maxConns,
maxFreeSockets: this.clientConfiguration.maxConns,
timeout,
// release socket for usage after 4s of inactivity. Must be less than Fauna's server
// side idle timeout of 5 seconds.
freeSocketTimeout: 4000,
};
let httpAgents;
if (this.clientConfiguration.endpoint.protocol === "http") {
httpAgents = { httpAgent: new Agent(agentSettings) };
} else {
httpAgents = { httpsAgent: new HttpsAgent(agentSettings) };
}
this.client = axios.create({
baseURL: this.clientConfiguration.endpoint.toString(),
timeout,
...httpAgents,
});
this.client.defaults.headers.common[
"Authorization"
] = `Bearer ${this.clientConfiguration.secret}`;
}

/**
* Queries Fauna.
* @param queryRequest - the {@link QueryRequest}
* @returns A {@link QueryResponse}.
* @throws A {@link QueryError} if the request cannnot be completed.
*/
async query<T = any>(queryRequest: QueryRequest): Promise<QueryResponse<T>> {
try {
const result = await this.client.post<QueryResponse<T>>(
"/query/1",
queryRequest
);
return result.data;
} catch (e) {
throw new QueryError("Query failed.");
}
}
}
32 changes: 32 additions & 0 deletions src/wire-protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* A request to make to Fauna.
*/
export interface QueryRequest {
/** The query. */
query: string;
}

/**
* A response to a query.
* @remarks
* The QueryResponse is type parameterized so that you can treat it as a
* a certain type if you are using Typescript.
*/
export interface QueryResponse<T> {
/**
* The result of the query. The data is any valid JSON value.
* @remarks
* data is type parameterized so that you can treat it as a
* certain type if you are using typescript.
*/
data: T;
/** Stats on query performance and cost */
stats: any;
/** The last transaction time of the query. An ISO-8601 date string. */
txn_time: string;
}

/**
* An error representing a query failure.
*/
export class QueryError extends Error {}
Loading

0 comments on commit df33094

Please sign in to comment.