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.
65 changes: 58 additions & 7 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 { fql, Page, 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,19 +15,67 @@ describe.each`
${"query"}
${"paginate"}
`("$method typings", ({ method }: { method: string }) => {
it("allows customers to use their own types", async () => {
expect.assertions(1);
// added in a junk property that is not part of QueryValue
type MyType = { x: number; t: QueryCheckError };
it("allows customers to use their own types in queries", async () => {
const query = fql`{ "x": 123 }`;
const paginatedQuery = fql`[{ "x": 123}].toSet()`;

if ("query" === method) {
const result = (await client.query<MyType>(fql`{ "x": 123}`)).data;
const result = (await client.query<MyType>(query)).data;
expect(result).toEqual({ x: 123 });
} else {
for await (const page of client.paginate<MyType>(fql`{ "x": 123}`)) {
expect.assertions(2);
for await (const page of client.paginate<MyType>(paginatedQuery)) {
for (const result of page) {
expect(result).toEqual({ x: 123 });
}
}

// It is also allowed to provide a query that does not return a page.
// When this happenes, the driver treats the result as if a page with
// exactly one item is returned.
for await (const page of client.paginate<MyType>(query)) {
for (const result of page) {
expect(result).toEqual({ x: 123 });
}
}
}
});

it("allows customers to infer their own types in queries from fql statements", async () => {
const query = fql<MyType>`{ "x": 123 }`;
const paginatedQuery = fql<Page<MyType>>`[{ "x": 123}].toSet()`;

if ("query" === method) {
const result = (await client.query(query)).data;
expect(result).toEqual({ x: 123 });
} else {
expect.assertions(2);
for await (const page of client.paginate(paginatedQuery)) {
for (const result of page) {
expect(result).toEqual({ x: 123 });
}
}

// It is also allowed to provide a query that does not return a page.
// When this happenes, the driver treats the result as if a page with
// exactly one item is returned.
for await (const page of client.paginate(query)) {
for (const result of page) {
expect(result).toEqual({ x: 123 });
}
}
}
});

it("allows customers to use subtyped queries", async () => {
const query = fql<string>`"hello"`;

const result = (await client.query<string | number>(query)).data;
expect(result).toEqual("hello");

// And make sure that the opposite is not possible
const query2 = fql<string | number>`"hello"`;
// @ts-expect-error Argument of type 'Query<string | number>' is not assignable to parameter of type 'Query<string>'.
await client.query<string>(query2);
});
Comment on lines +78 to +79
Copy link
Contributor

Choose a reason for hiding this comment

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

wow cool assertion here.

Copy link
Contributor

@cleve-fauna cleve-fauna Jul 29, 2024

Choose a reason for hiding this comment

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

Would you mind adding how to use this to the development section of the README?

As we tweak type arguments in future work capturing that we can sanity check our types using this and proper linter settings will help out future devs.

For example, we got some work we'd like to do to make less boilerplate for users when definining their own types and when using .query calls that return pages (as opposed to using the clients pagination utils - there's cases you might do that in an API for example). We have some of these type improvements documented internally here: https://faunadb.atlassian.net/wiki/spaces/DX/pages/3852697604/Developer+Tooling+Pain#API-Pagination

So this test pattern is something we will likely need aagain.

Just linking here: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html with some text about the linter settings needed would suffice.

});
Loading
Loading