Skip to content

Commit

Permalink
Separate QueryOptions from QueryRequest (#162)
Browse files Browse the repository at this point in the history
Co-authored-by: Cleve Stuart <90649124+cleve-fauna@users.noreply.github.com>
  • Loading branch information
ptpaterson and cleve-fauna authored May 22, 2023
1 parent 65b897f commit 5ad59b3
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 65 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,11 @@ console.assert(user_doc.email === "alice@site.example");
Options are available to configure queries on each request.

```typescript
import { fql, Client, type QueryRequestHeaders } from "fauna";
import { fql, Client, type QueryOptions } from "fauna";

const client = new Client();

const options: QueryRequestHeaders = {
const options: QueryOptions = {
format: "tagged",
linearized: false,
query_timeout_ms: 60_000,
Expand Down
29 changes: 27 additions & 2 deletions __tests__/integration/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,31 @@ describe("query", () => {
}
);

it("can send arguments directly", async () => {
const foo = {
double: 4.14,
int: 32,
string: "foo",
null: null,
object: { foo: "bar" },
array: [1, 2, 3],
"@tagged": "tagged",
};

const response = await client.query<typeof foo>(fql`foo`, {
arguments: { foo },
});
const foo2 = response.data;

expect(foo2.double).toBe(4.14);
expect(foo2.int).toBe(32);
expect(foo2.string).toBe("foo");
expect(foo2.null).toBeNull();
expect(foo2.object).toStrictEqual({ foo: "bar" });
expect(foo2.array).toStrictEqual([1, 2, 3]);
expect(foo2["@tagged"]).toBe("tagged");
});

it("throws a QueryCheckError if the query is invalid", async () => {
expect.assertions(4);
try {
Expand Down Expand Up @@ -422,7 +447,7 @@ describe("query can encode / decode QueryValue correctly", () => {
Collection.create({ name: ${collectionName}})
}`);
// whack in undefined
// @ts-ignore
// @ts-expect-error Type 'undefined' is not assignable to type 'QueryValue'
let toughInput: QueryValue = {
foo: "bar",
shouldnt_exist: undefined,
Expand All @@ -449,7 +474,7 @@ describe("query can encode / decode QueryValue correctly", () => {
Collection.create({ name: ${collectionName}})
}`);
// whack in undefined
// @ts-ignore
// @ts-expect-error Type 'undefined' is not assignable to type 'QueryValue'
let undefinedValue: QueryValue = undefined;
try {
const docCreated = await client.query(fql`
Expand Down
3 changes: 2 additions & 1 deletion src/client-configuration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ValueFormat } from "./wire-protocol";

/**
* Configuration for a client.
* Configuration for a client. The options provided are used as the
* default options for each query.
*/
export interface ClientConfiguration {
/**
Expand Down
93 changes: 50 additions & 43 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,28 @@ import { EmbeddedSet, Page, SetIterator } from "./values";
import {
isQueryFailure,
isQuerySuccess,
QueryInterpolation,
type QueryFailure,
type QueryRequest,
type QueryRequestHeaders,
type QueryOptions,
type QuerySuccess,
type QueryValue,
type ValueFormat,
} from "./wire-protocol";

interface RequiredClientConfig {
client_timeout_buffer_ms: number;
endpoint: URL;
format: ValueFormat;
http2_session_idle_ms: number;
http2_max_streams: number;
fetch_keepalive: boolean;
secret: string;
query_timeout_ms: number;
}
type RequiredClientConfig = ClientConfiguration &
Required<
Pick<
ClientConfiguration,
| "client_timeout_buffer_ms"
| "endpoint"
| "fetch_keepalive"
| "http2_max_streams"
| "http2_session_idle_ms"
| "secret"
// required default query options
| "format"
| "query_timeout_ms"
>
>;

const DEFAULT_CLIENT_CONFIG: Omit<
ClientConfiguration & RequiredClientConfig,
Expand All @@ -69,7 +73,7 @@ export class Client {
static readonly #driverEnvHeader = getDriverEnv();

/** The {@link ClientConfiguration} */
readonly #clientConfiguration: ClientConfiguration & RequiredClientConfig;
readonly #clientConfiguration: RequiredClientConfig;
/** The underlying {@link HTTPClient} client. */
readonly #httpClient: HTTPClient;
/** The last transaction timestamp this client has seen */
Expand Down Expand Up @@ -213,7 +217,7 @@ export class Client {
* @param request - a {@link Query} to execute in Fauna.
* Note, you can embed header fields in this object; if you do that there's no need to
* pass the headers parameter.
* @param headers - optional {@link QueryRequestHeaders} to apply on top of the request input.
* @param headers - optional {@link QueryOptions} to apply on top of the request input.
* Values in this headers parameter take precedence over the same values in the {@link ClientConfiguration}.
* @returns Promise&lt;{@link QuerySuccess}&gt;.
*
Expand All @@ -233,16 +237,22 @@ export class Client {
* due to an internal error.
*/
async query<T extends QueryValue>(
request: Query,
headers?: QueryRequestHeaders
query: Query,
options?: QueryOptions
): Promise<QuerySuccess<T>> {
if (this.#isClosed) {
throw new ClientClosedError(
"Your client is closed. No further requests can be issued."
);
}

return this.#query(request.toQuery(headers));
// QueryInterpolation values must always be encoded.
// TODO: The Query implementation never set the QueryRequest arguments.
// When we separate query building from query encoding we should be able
// to simply do `const queryInterpolation: TaggedTypeFormat.encode(query)`
const queryInterpolation = query.toQuery(options).query;

return this.#query(queryInterpolation, options);
}

#getError(e: any): ClientError | NetworkError | ProtocolError | ServiceError {
Expand Down Expand Up @@ -335,49 +345,43 @@ in an environmental variable named FAUNA_SECRET or pass it to the Client\
}
}

async #query<T extends QueryValue = any>(
queryRequest: QueryRequest
async #query<T extends QueryValue>(
queryInterpolation: string | QueryInterpolation,
options?: QueryOptions
): Promise<QuerySuccess<T>> {
try {
const requestConfig = {
...this.#clientConfiguration,
...options,
};

const headers = {
Authorization: `Bearer ${this.#clientConfiguration.secret}`,
};
this.#setHeaders(
{ ...this.clientConfiguration, ...queryRequest },
headers
);
this.#setHeaders(requestConfig, headers);

const requestConfig: QueryRequestHeaders = {
...this.#clientConfiguration,
...queryRequest,
};
const isTaggedFormat = requestConfig.format === "tagged";

const isTaggedFormat =
requestConfig.format === "tagged" || queryRequest.format === "tagged";
const queryArgs = isTaggedFormat
? TaggedTypeFormat.encode(queryRequest.arguments)
: queryRequest.arguments;
const queryArgs = requestConfig.arguments
? isTaggedFormat
? TaggedTypeFormat.encode(requestConfig.arguments)
: requestConfig.arguments
: undefined;

const requestData = {
query: queryRequest.query,
query: queryInterpolation,
arguments: queryArgs,
};

// TODO: We know we are providing a default for query_timeout_ms, so can
// cast to a defined value. Types for QueryRequest is too tangled up with
// QueryRequestHeaders to set query_timeout_ms as a mandatory field. #144
// should fix that.
const client_timeout_ms =
(requestConfig.query_timeout_ms as number) +
requestConfig.query_timeout_ms +
this.#clientConfiguration.client_timeout_buffer_ms;

const fetchResponse = await this.#httpClient.request({
// required
client_timeout_ms,
data: requestData,
headers,
method: "POST",
// optional
client_timeout_ms,
});

let parsedResponse;
Expand Down Expand Up @@ -422,7 +426,10 @@ in an environmental variable named FAUNA_SECRET or pass it to the Client\
}
}

#setHeaders(fromObject: QueryRequestHeaders, headerObject: any): void {
#setHeaders(
fromObject: QueryOptions,
headerObject: Record<string, string | number>
): void {
for (const entry of Object.entries(fromObject)) {
if (
[
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export { Client } from "./client";
export {
endpoints,
type ClientConfiguration,
type Endpoints,
endpoints,
} from "./client-configuration";
export {
AbortError,
Expand Down Expand Up @@ -30,8 +30,8 @@ export {
type QueryFailure,
type QueryInfo,
type QueryInterpolation,
type QueryOptions,
type QueryRequest,
type QueryRequestHeaders,
type QueryStats,
type QuerySuccess,
type Span,
Expand Down
8 changes: 4 additions & 4 deletions src/query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
QueryValue,
QueryInterpolation,
QueryRequest,
QueryRequestHeaders,
QueryOptions,
} from "./wire-protocol";

/**
Expand Down Expand Up @@ -56,7 +56,7 @@ export class Query {
/**
* Converts this Query to a {@link QueryRequest} you can send
* to Fauna.
* @param requestHeaders - optional {@link QueryRequestHeaders} to include
* @param requestHeaders - optional {@link QueryOptions} to include
* in the request (and thus override the defaults in your {@link ClientConfiguration}.
* If not passed in, no headers will be set as overrides.
* @returns a {@link QueryRequest}.
Expand All @@ -69,11 +69,11 @@ export class Query {
* { query: { fql: ["'foo'.length == ", { value: { "@int": "8" } }, ""] }}
* ```
*/
toQuery(requestHeaders: QueryRequestHeaders = {}): QueryRequest {
toQuery(requestHeaders: QueryOptions = {}): QueryRequest {
return { ...this.#render(requestHeaders), ...requestHeaders };
}

#render(requestHeaders: QueryRequestHeaders): QueryRequest {
#render(requestHeaders: QueryOptions): QueryRequest {
if (this.#queryFragments.length === 1) {
return { query: { fql: [this.#queryFragments[0]] }, arguments: {} };
}
Expand Down
38 changes: 27 additions & 11 deletions src/wire-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
/**
* A request to make to Fauna.
*/
export interface QueryRequest extends QueryRequestHeaders {
export interface QueryRequest {
/** The query */
query: string | QueryInterpolation;

Expand All @@ -26,45 +26,61 @@ export interface QueryRequest extends QueryRequestHeaders {
arguments?: QueryValueObject;
}

export interface QueryRequestHeaders {
/**
* Options for queries. Each query can be made with different options. Settings here
* take precedence over those in {@link ClientConfiguration}.
*/
export interface QueryOptions {
/** Optional arguments. Variables in the query will be initialized to the
* value associated with an argument key.
*/
arguments?: QueryValueObject;

/**
* Determines the encoded format expected for the query `arguments` field, and
* the `data` field of a successful response.
* Overrides the optional setting on the {@link ClientConfiguration}.
*/
format?: ValueFormat;

/**
* If true, unconditionally run the query as strictly serialized.
* This affects read-only transactions. Transactions which write
* will always be strictly serialized.
* Overrides the optional setting for the client.
* Overrides the optional setting on the {@link ClientConfiguration}.
*/
linearized?: boolean;
/**
* The timeout to use in this query in milliseconds.
* Overrides the timeout for the client.
*/
query_timeout_ms?: number;

/**
* The max number of times to retry the query if contention is encountered.
* Overrides the optional setting for the client.
*Overrides the optional setting on the {@link ClientConfiguration}.
*/
max_contention_retries?: number;

/**
* Tags provided back via logging and telemetry.
* Overrides the optional setting on the client.
* Overrides the optional setting on the {@link ClientConfiguration}.
*/
query_tags?: Record<string, string>;

/**
* The timeout to use in this query in milliseconds.
* Overrides the optional setting on the {@link ClientConfiguration}.
*/
query_timeout_ms?: number;

/**
* A traceparent provided back via logging and telemetry.
* Must match format: https://www.w3.org/TR/trace-context/#traceparent-header
* Overrides the optional setting for the client.
* Overrides the optional setting on the {@link ClientConfiguration}.
*/
traceparent?: string;

/**
* Enable or disable typechecking of the query before evaluation. If no value
* is provided, the value of `typechecked` in the database configuration will
* be used.
* Overrides the optional setting on the {@link ClientConfiguration}.
*/
typecheck?: boolean;
}
Expand Down

0 comments on commit 5ad59b3

Please sign in to comment.