Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add backwards compatible generics support for fql statements #277

Merged
merged 9 commits into from
Jul 25, 2024
76 changes: 39 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ See the [Fauna Documentation](https://docs.fauna.com/fauna/current/) for additio

</details>


## Supported runtimes

**Server-side**
Expand All @@ -67,10 +66,9 @@ Stable versions of:
- Safari 12.1+
- Edge 79+


## Install

The driver is available on [npm](https://www.npmjs.com/package/fauna). You
The driver is available on [npm](https://www.npmjs.com/package/fauna). You
can install it using your preferred package manager. For example:

```shell
Expand All @@ -85,7 +83,6 @@ Browsers can import the driver using a CDN link:
</script>
```


## Usage

By default, the driver's `Client` instance authenticates with Fauna using an
Expand Down Expand Up @@ -134,7 +131,6 @@ try {
}
```


### Write FQL queries

The `fql` function is your gateway to building safe, reuseable Fauna queries.
Expand Down Expand Up @@ -175,8 +171,7 @@ This has several advantages:

- You can use `fql` to build a library of subqueries applicable to your domain - and combinable in whatever way you need
- Injection attacks are not possible if you pass input variables into the interpolated (`` `${interpoloated_argument}` ``) parts of the query.
- The driver speaks "pure" FQL - you can try out some FQL queries on the dashboard's terminal and paste it directly into your app like `` fql`copied from terminal...` `` and the query will work as is.

- The driver speaks "pure" FQL - you can try out some FQL queries on the dashboard's terminal and paste it directly into your app like ``fql`copied from terminal...` `` and the query will work as is.

### Typescript support

Expand All @@ -198,14 +193,37 @@ const query = fql`{
}`;

const response: QuerySuccess<User> = await client.query<User>(query);
const user_doc: User = response.data;
const userDoc: User = response.data;

console.assert(user_doc.name === "Alice");
console.assert(user_doc.email === "[email protected]");
console.assert(userDoc.name === "Alice");
console.assert(userDoc.email === "[email protected]");

client.close();
```

Alternatively, you can apply a type parameter directly to your
fql statements and `Client` methods will infer your return types.
Due to backwards compatibility, if a type parameter is provided to
`Client` methods, it will override the inferred type from your
query.

```typescript
const query = fql<User>`{
name: "Alice",
email: "[email protected]",
}`;

// response will be typed as QuerySuccess<User>
const response = await client.query(query);

// userDoc will be automatically inferred as User
const userDoc = response.data;

console.assert(userDoc.name === "Alice");
console.assert(userDoc.email === "[email protected]");

client.close();
```

### Query options

Expand All @@ -230,12 +248,11 @@ const options: QueryOptions = {
};

const response = await client.query(fql`"Hello, #{name}!"`, options);
console.log(response.data)
console.log(response.data);

client.close();
```


### Query statistics

Query statistics are returned with successful query responses and errors of
Expand All @@ -255,7 +272,7 @@ const client = new Client();

try {
const response: QuerySuccess<string> = await client.query<string>(
fql`"Hello world"`
fql`"Hello world"`,
);
const stats: QueryStats | undefined = response.stats;
console.log(stats);
Expand Down Expand Up @@ -307,7 +324,7 @@ const pages: SetIterator<QueryValue> = client.paginate(query, options);

for await (const products of pages) {
for (const product of products) {
console.log(product)
console.log(product);
}
}

Expand All @@ -320,7 +337,7 @@ Use `flatten()` to get paginated results as a single, flat array:
const pages: SetIterator<QueryValue> = client.paginate(query, options);

for await (const product of pages.flatten()) {
console.log(product)
console.log(product);
}
```

Expand Down Expand Up @@ -360,7 +377,6 @@ const config: ClientConfiguration = {
const client = new Client(config);
```


### Environment variables

The driver will default to configuring your client with the values of the `FAUNA_SECRET` and `FAUNA_ENDPOINT` environment variable.
Expand All @@ -378,27 +394,22 @@ You can initalize the client with a default configuration:
const client = new Client();
```


### Retry


#### Max attempts

The maximum number of times a query will be attempted if a retryable exception is thrown (ThrottlingError). Default 3, inclusive of the initial call. The retry strategy implemented is a simple exponential backoff.

To disable retries, pass max_attempts less than or equal to 1.


#### Max backoff

The maximum backoff in seconds to be observed between each retry. Default 20 seconds.


### Timeouts

There are a few different timeout settings that can be configured; each comes with a default setting. We recommend that most applications simply stick to the defaults.


#### Query timeout

The query timeout is the time, in milliseconds, that Fauna will spend executing your query before aborting with a 503 Timeout error. If a query timeout occurs, the driver will throw an instance of `QueryTimeoutError`.
Expand All @@ -417,7 +428,6 @@ when performing this query.
const response = await client.query(myQuery, { query_timeout_ms: 20_000 });
```


#### Client timeout

The client timeout is the time, in milliseconds, that the client will wait for a network response before canceling the request. If a client timeout occurs, the driver will throw an instance of `NetworkError`.
Expand All @@ -428,7 +438,6 @@ The client timeout is always the query timeout plus an additional buffer. This e
const client = new Client({ client_timeout_buffer_ms: 6000 });
```


#### HTTP/2 session idle timeout

The HTTP/2 session idle timeout is the time, in milliseconds, that an HTTP/2 session will remain open after there is no more pending communication. Once the session idle time has elapsed the session is considered idle and the session is closed. Subsequent requests will create a new session; the session idle timeout does not result in an error.
Expand Down Expand Up @@ -473,15 +482,15 @@ const response = await client.query(fql`
`);
const { initialPage, streamToken } = response.data;

client.stream(streamToken)
client.stream(streamToken);
```

You can also pass a query that produces a stream token directly to `stream()`:

```javascript
const query = fql`Product.all().changesOn(.price, .quantity)`
const query = fql`Product.all().changesOn(.price, .quantity)`;

client.stream(query)
client.stream(query);
```

### Iterate on a stream
Expand All @@ -504,7 +513,6 @@ try {
// An error will be handled here if Fauna returns a terminal, "error" event, or
// if Fauna returns a non-200 response when trying to connect, or
// if the max number of retries on network errors is reached.

// ... handle fatal error
}
```
Expand All @@ -527,9 +535,8 @@ stream.start(
// An error will be handled here if Fauna returns a terminal, "error" event, or
// if Fauna returns a non-200 response when trying to connect, or
// if the max number of retries on network errors is reached.

// ... handle fatal error
}
},
);
```

Expand All @@ -538,7 +545,7 @@ stream.start(
Use `close()` to close a stream:

```javascript
const stream = await client.stream(fql`Product.all().toStream()`)
const stream = await client.stream(fql`Product.all().toStream()`);

let count = 0;
for await (const event of stream) {
Expand All @@ -548,7 +555,7 @@ for await (const event of stream) {

// Close the stream after 2 events
if (count === 2) {
stream.close()
stream.close();
break;
}
}
Expand All @@ -570,14 +577,13 @@ const options = {
status_events: true,
};

client.stream(fql`Product.all().toStream()`, options)
client.stream(fql`Product.all().toStream()`, options);
```

For supported properties, see [Stream
options](https://docs.fauna.com/fauna/current/drivers/js-client#stream-options)
in the Fauna docs.


## Contributing

Any contributions are from the community are greatly appreciated!
Expand All @@ -586,26 +592,22 @@ If you have a suggestion that would make this better, please fork the repo and c

Don't forget to give the project a star! Thanks again!


### Set up the repo

1. Clone the repository; e.g. `gh repo clone fauna/fauna-js` if you use the GitHub CLI
2. Install dependencies via `yarn install`


### Run tests

1. Start a docker desktop or other docker platform.
2. Run `yarn test`. This will start local fauna containers, verify they're up and run all tests.


### Lint your code

Linting runs automatically on each commit.

If you wish to run on-demand run `yarn lint`.


## License

Distributed under the MPL 2.0 License. See [LICENSE](./LICENSE) for more information.
31 changes: 28 additions & 3 deletions __tests__/integration/query-typings.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { fql, QueryCheckError } from "../../src";
import { getClient } from "../client";

// added in a junk property that is not part of QueryValue
type MyType = { x: number; t: QueryCheckError };

const client = getClient();

afterAll(() => {
Expand All @@ -12,10 +15,8 @@ describe.each`
${"query"}
${"paginate"}
`("$method typings", ({ method }: { method: string }) => {
it("allows customers to use their own types", async () => {
it("allows customers to use their own types in queries", async () => {
expect.assertions(1);
// added in a junk property that is not part of QueryValue
type MyType = { x: number; t: QueryCheckError };
if ("query" === method) {
const result = (await client.query<MyType>(fql`{ "x": 123}`)).data;
expect(result).toEqual({ x: 123 });
Expand All @@ -27,4 +28,28 @@ describe.each`
}
}
});

it("allows customers to infer their own types in queries from fql statements", async () => {
// This is a noop function that is only used to validate the inferred type of the query
// It will fail at build time if types are not inferred correctly.
const noopToValidateInferredType = (value: MyType) => {};

const query = fql<MyType>`{ "x": 123 }`;
const q2 = fql`{ "x": ${query} }`;

expect.assertions(1);
if ("query" === method) {
const result = (await client.query(query)).data;
noopToValidateInferredType(result);
expect(result).toEqual({ x: 123 });
} else {
for await (const page of client.paginate<MyType>(fql`{ "x": 123}`)) {
ecooper marked this conversation as resolved.
Show resolved Hide resolved
for (const result of page) {
noopToValidateInferredType(result);
expect(result).toEqual({ x: 123 });
}
}
}
Promise.resolve();
});
});
19 changes: 11 additions & 8 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export class Client {
* max_attempts, inclusive of the initial call.
*
* @typeParam T - The expected type of the items returned from Fauna on each
* iteration
* iteration. T can be inferred if the provided query used a type parameter.
* @param iterable - a {@link Query} or an existing fauna Set ({@link Page} or
* {@link EmbeddedSet})
* @param options - a {@link QueryOptions} to apply to the queries. Optional.
Expand Down Expand Up @@ -211,7 +211,7 @@ export class Client {
* ```
*/
paginate<T extends QueryValue>(
iterable: Page<T> | EmbeddedSet | Query,
iterable: Page<T> | EmbeddedSet | Query<T>,
ecooper marked this conversation as resolved.
Show resolved Hide resolved
options?: QueryOptions,
): SetIterator<T> {
if (iterable instanceof Query) {
Expand All @@ -224,10 +224,11 @@ export class Client {
* Queries Fauna. Queries will be retried in the event of a ThrottlingError up to the client's configured
* max_attempts, inclusive of the initial call.
*
* @typeParam T - The expected type of the response from Fauna
* @typeParam T - The expected type of the response from Fauna. T can be inferred if the
* provided query used a type parameter.
* @param query - 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.
* Note, you can embed header fields in this object; if you do that there's no need to
* pass the headers parameter.
* @param options - 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 @@ -245,7 +246,7 @@ export class Client {
* due to an internal error.
*/
async query<T extends QueryValue>(
query: Query,
query: Query<T>,
options?: QueryOptions,
): Promise<QuerySuccess<T>> {
if (this.#isClosed) {
Expand All @@ -269,9 +270,11 @@ export class Client {

/**
* Initialize a streaming request to Fauna
* @typeParam T - The expected type of the response from Fauna. T can be inferred
* if theprovided query used a type parameter.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/s/theprovided/the provided/

* @param query - A string-encoded streaming token, or a {@link Query}
* @returns A {@link StreamClient} that which can be used to listen to a stream
* of events
* of events
*
* @example
* ```javascript
Expand Down Expand Up @@ -323,7 +326,7 @@ export class Client {
* ```
*/
stream<T extends QueryValue>(
tokenOrQuery: StreamToken | Query,
tokenOrQuery: StreamToken | Query<T>,
options?: Partial<StreamClientConfiguration>,
): StreamClient<T> {
if (this.#isClosed) {
Expand Down
Loading
Loading