Skip to content

Commit

Permalink
Merge branch 'master' into export-condition-module-sync
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinTail authored Feb 5, 2025
2 parents c93c0ee + 6cc5354 commit 3e285ed
Show file tree
Hide file tree
Showing 53 changed files with 2,831 additions and 1,685 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ name: "CodeQL"

on:
push:
branches: [ master, v19, v20, v21 ]
branches: [ master, v18, v19, v20, v21 ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master, v19, v20, v21 ]
branches: [ master, v18, v19, v20, v21 ]
schedule:
- cron: '26 8 * * 1'

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Node.js CI

on:
push:
branches: [ master, v19, v20, v21 ]
branches: [ master, v18, v19, v20, v21 ]
pull_request:
branches: [ master, v19, v20, v21 ]
branches: [ master, v18, v19, v20, v21 ]

jobs:
build:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: OpenAPI Validation

on:
push:
branches: [ master, v19, v20, v21 ]
branches: [ master, v18, v19, v20, v21 ]
pull_request:
branches: [ master, v19, v20, v21 ]
branches: [ master, v18, v19, v20, v21 ]


jobs:
Expand Down
167 changes: 164 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,173 @@

## Version 22

### v22.6.0

- Feature: pulling examples up from the object schema properties:
- When describing I/O schemas for generating `Documentation` the examples used to work properly only when assigned to
the top level (`z.object().example()`), especially complex scenarios involving path parameters and middlewares;
- This version supports examples assigned to the individual properties on the I/O object schemas;
- It makes the syntax more readable and fixes the issue when example is only set for a path parameter.

```ts
const before = factory.build({
input: z
.object({
key: z.string(),
})
.example({
key: "1234-5678-90",
}),
});

const after = factory.build({
input: z.object({
key: z.string().example("1234-5678-90"),
}),
});
```

### v22.5.0

- Feature: `defaultResultHandler` sets headers from `HttpError`:
- If you `throw createHttpError(400, "message", { headers })` those `headers` go to the negative response.
- Feature: Ability to respond with status code `405` (Method not allowed) to requests having wrong method:
- Previously, in all cases where the method and route combination was not defined, the response had status code `404`;
- For situations where a known route does not support the method being used, there is a more appropriate code `405`:
- See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 for details;
- You can activate this feature by setting the new `wrongMethodBehavior` config option `405` (default: `404`).

```ts
import { createConfig } from "express-zod-api";

createConfig({ wrongMethodBehavior: 405 });
```

### v22.4.2

- Excluded 41 response-only headers from the list of well-known ones used to depict request params in Documentation.

### v22.4.1

- Fixed a bug that could lead to duplicate properties in generated client types:
- If the middleware and/or endpoint schemas had the same property, it was duplicated by Integration.
- The issue was introduced in [v20.15.3](#v20153) and reported by [@bobgubko](https://github.com/bobgubko).

```ts
// reproduction
factory
.addMiddleware({
input: z.object({ query: z.string() }), // ...
})
.build({
input: z.object({ query: z.string() }), // ...
});
```

```ts
type Before = {
query: string;
query: string; // <— bug #2352
};
type After = {
query: string;
};
```

### v22.4.0

- Feat: ability to supply extra data to a custom implementation of the generated client:
- You can instantiate the client class with an implementation accepting an optional context of your choice;
- The public `.provide()` method can now accept an additional argument having the type of that context;
- The problem on missing such ability was reported by [@LucWag](https://github.com/LucWag).

```ts
import { Client, Implementation } from "./generated-client.ts";

interface MyCtx {
extraKey: string;
}

const implementation: Implementation<MyCtx> = async (
method,
path,
params,
ctx, // ctx is optional MyCtx
) => {};

const client = new Client(implementation);

client.provide("get /v1/user/retrieve", { id: "10" }, { extraKey: "123456" });
```

### v22.3.1

- Fixed issue on emitting server-sent events (SSE), introduced in v21.5.0:
- Emitting SSE failed due to internal error `flush is not a function` having `compression` disabled in config;
- The `.flush()` method of `response` is a feature of `compression` (optional peer dependency);
- It is required to call the method when `compression` is enabled;
- This version fixes the issue by calling the method conditionally;
- This bug was reported by [@bobgubko](https://github.com/bobgubko).

### v22.3.0

- Feat: `Subscription` class for consuming Server-sent events:
- The `Integration` can now also generate a frontend helper class `Subscription` to ease SSE support;
- The new class establishes an `EventSource` instance and exposes it as the public `source` property;
- The class also provides the public `on` method for your typed listeners;
- You can configure the generated class name using `subscriptionClassName` option (default: `Subscription`);
- The feature is only applicable to the `variant` option set to `client` (default).

```ts
import { Subscription } from "./client.ts"; // the generated file

new Subscription("get /v1/events/stream", {}).on("time", (time) => {});
```

### v22.2.0

- Feat: detecting headers from `Middleware::security` declarations:
- When `headers` are enabled within `inputSources` of config, the `Documentation` generator can now identify them
among other inputs additionally by using the security declarations of middlewares attached to an `Endpoint`;
- This approach enables handling of custom headers without `x-` prefix.

```ts
const authMiddleware = new Middleware({
security: { type: "header", name: "token" },
});
```

### v22.1.1

- This version contains an important technical simplification of routines related to processing of `security`
declarations of the used `Middleware` when generating API `Documentation`.
- No changes to the operation are expected. This refactoring is required for a feature that will be released later.

### v22.1.0

- Feat: ability to configure the generated client class name:
- New option `clientClassName` for `Integration::constructor()` argument, default: `Client`.
- Feat: default implementation for the generated client:
- The argument of the generated client class constructor became optional;
- The `Implementation` previously suggested as an example (using `fetch`) became the one used by default;
- You may no longer need to write the implementation if the default one suits your needs.

```ts
import { Integration } from "express-zod-api";
new Integration({ clientClassName: "FancyClient" });
```

```ts
import { FancyClient } from "./generated-client.ts";
const client = new FancyClient(/* optional implementation */);
```

### v22.0.0

- Minimum supported Node versions: 20.9.0 and 22.0.0:
- Node 18 is no longer supported, its end of life is April 30, 2025.
- Node 18 is no longer supported; its end of life is April 30, 2025.
- `BuiltinLogger::profile()` behavior changed for picoseconds: expressing them through nanoseconds;
- Feature: handling all headers as input source (when enabled):
- Feature: handling all (not just `x-` prefixed) headers as an input source (when enabled):
- Behavior changed for `headers` inside `inputSources` config option: all headers are addressed to the `input` object;
- This change is motivated by the deprecation of `x-` prefixed headers;
- Since the order inside `inputSources` matters, consider moving `headers` to the first place to avoid overwrites;
Expand All @@ -19,7 +180,7 @@
- The overload of the `Client::provide()` having 3 arguments and the `Provider` type are removed;
- The public `jsonEndpoints` const is removed — use the `content-type` header of an actual response instead;
- The public type `MethodPath` is removed — use the `Request` type instead.
- The approach on tagging endpoints changed:
- The approach to tagging endpoints changed:
- The `tags` property moved from the argument of `createConfig()` to `Documentation::constructor()`;
- The overload of `EndpointsFactory::constructor()` accepting `config` property is removed;
- The argument of `EventStreamFactory::constructor()` is now the events map (formerly assigned to `events` property);
Expand Down
59 changes: 24 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ Therefore, many basic tasks can be accomplished faster and easier, in particular

These people contributed to the improvement of the framework by reporting bugs, making changes and suggesting ideas:

[<img src="https://github.com/bobgubko.png" alt="@bobgubko" width="50px" />](https://github.com/bobgubko)
[<img src="https://github.com/LucWag.png" alt="@LucWag" width="50px" />](https://github.com/LucWag)
[<img src="https://github.com/HenriJ.png" alt="@HenriJ" width="50px" />](https://github.com/HenriJ)
[<img src="https://github.com/JonParton.png" alt="@JonParton" width="50px" />](https://github.com/JonParton)
[<img src="https://github.com/williamgcampbell.png" alt="@williamgcampbell" width="50px" />](https://github.com/williamgcampbell)
Expand All @@ -102,7 +104,6 @@ These people contributed to the improvement of the framework by reporting bugs,
[<img src="https://github.com/danclaytondev.png" alt="@danclaytondev" width="50px" />](https://github.com/danclaytondev)
[<img src="https://github.com/huyhoang160593.png" alt="@huyhoang160593" width="50px" />](https://github.com/huyhoang160593)
[<img src="https://github.com/sarahssharkey.png" alt="@sarahssharkey" width="50px" />](https://github.com/sarahssharkey)
[<img src="https://github.com/bobgubko.png" alt="@bobgubko" width="50px" />](https://github.com/bobgubko)
[<img src="https://github.com/master-chu.png" alt="@master-chu" width="50px" />](https://github.com/master-chu)
[<img src="https://github.com/alindsay55661.png" alt="@alindsay55661" width="50px" />](https://github.com/alindsay55661)
[<img src="https://github.com/john-schmitz.png" alt="@john-schmitz" width="50px" />](https://github.com/john-schmitz)
Expand Down Expand Up @@ -731,10 +732,11 @@ createConfig({
In a similar way you can enable request headers as the input source. This is an opt-in feature. Please note:

- consider giving `headers` the lowest priority among other `inputSources` to avoid overwrites;
- consider handling headers in `Middleware` and declaring them within `security` property to improve `Documentation`;
- the request headers acquired that way are always lowercase when describing their validation schemas.

```typescript
import { createConfig, defaultEndpointsFactory } from "express-zod-api";
import { createConfig, Middleware } from "express-zod-api";
import { z } from "zod";

createConfig({
Expand All @@ -743,7 +745,12 @@ createConfig({
}, // ...
});

defaultEndpointsFactory.build({
new Middleware({
security: { type: "header", name: "token" }, // recommended
input: z.object({ token: z.string() }),
});

factory.build({
input: z.object({
"x-request-id": z.string(), // this one is from request.headers
id: z.string(), // this one is from request.query
Expand Down Expand Up @@ -898,7 +905,8 @@ origins of errors that could happen in runtime and be handled the following way:
- Others, inheriting from `Error` class (`500`);
- Ones related to routing, parsing and upload issues — handled by `ResultHandler` assigned to `errorHandler` in config:
- Default is `defaultResultHandler` — it sets the response status code from the corresponding `HttpError`:
`400` for parsing, `404` for routing, `config.upload.limitError.statusCode` for upload issues, or `500` for others.
`400` for parsing, `404` for routing, `config.upload.limitError.statusCode` for upload issues, or `500` for others;
- You can also set `wrongMethodBehavior` config option to `405` (Method not allowed) for requests having wrong method;
- `ResultHandler` must handle possible `error` and avoid throwing its own errors, otherwise:
- Ones related to `ResultHandler` execution — handled by `LastResortHandler`:
- Response status code is always `500` and the response itself is a plain text.
Expand Down Expand Up @@ -1196,8 +1204,9 @@ createConfig({

If you want the user of a client application to be able to subscribe to subsequent updates initiated by the server,
consider [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE) feature.
Client application can subscribe to the event stream using `EventSource` class instance. The following example
demonstrates the implementation emitting the `time` event each second.
Client application can subscribe to the event stream using `EventSource` class instance or the
[instance of the generated](#generating-a-frontend-client) `Subscription` class. The following example demonstrates
the implementation emitting the `time` event each second.

```typescript
import { z } from "zod";
Expand All @@ -1217,13 +1226,6 @@ const subscriptionEndpoint = EventStreamFactory({
});
```

```js
const source = new EventSource("https://example.com/api/v1/time");
source.addEventListener("time", (event) => {
const data = JSON.parse(event.data); // number
});
```

If you need more capabilities, such as bidirectional event sending, I have developed an additional websocket operating
framework, [Zod Sockets](https://github.com/RobinTail/zod-sockets), which has similar principles and capabilities.

Expand Down Expand Up @@ -1256,28 +1258,17 @@ const prettierFormattedTypescriptCode = await client.printFormatted(); // or jus
```

Alternatively, you can supply your own `format` function into that method or use a regular `print()` method instead.
The generated client is flexibly configurable on the frontend side using an implementation function that
directly makes requests to an endpoint using the libraries and methods of your choice.
The client asserts the type of request parameters and response.
Consuming the generated client requires Typescript version 4.1 or higher.
The generated client is flexibly configurable on the frontend side for using a custom implementation function that
makes requests using the libraries and methods of your choice. The default implementation uses `fetch`. The client
asserts the type of request parameters and response. Consuming the generated client requires Typescript version 4.1+.

```typescript
// example frontend, simple implementation based on fetch()
import { Client } from "./client.ts"; // the generated file

const client = new Client(async (method, path, params) => {
const hasBody = !["get", "delete"].includes(method);
const searchParams = hasBody ? "" : `?${new URLSearchParams(params)}`;
const response = await fetch(`https://example.com${path}${searchParams}`, {
method: method.toUpperCase(),
headers: hasBody ? { "Content-Type": "application/json" } : undefined,
body: hasBody ? JSON.stringify(params) : undefined,
});
return response.json();
});
import { Client, Implementation, Subscription } from "./client.ts"; // the generated file

const client = new Client(/* optional custom Implementation */);
client.provide("get /v1/user/retrieve", { id: "10" });
client.provide("post /v1/user/:id", { id: "10" }); // it also substitues path params
new Subscription("get /v1/events/stream", {}).on("time", (time) => {}); // Server-sent events (SSE)
```

## Creating a documentation
Expand Down Expand Up @@ -1307,11 +1298,9 @@ import { defaultEndpointsFactory } from "express-zod-api";
const exampleEndpoint = defaultEndpointsFactory.build({
shortDescription: "Retrieves the user.", // <—— this becomes the summary line
description: "The detailed explanaition on what this endpoint does.",
input: z
.object({
id: z.number().describe("the ID of the user"),
})
.example({ id: 123 }),
input: z.object({
id: z.number().describe("the ID of the user").example(123),
}),
// ..., similarly for output and middlewares
});
```
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Version | Release | Supported |
| ------: | :------ | :----------------: |
| 22.x.x | 02.2025 | :white_check_mark: |
| 22.x.x | 01.2025 | :white_check_mark: |
| 21.x.x | 11.2024 | :white_check_mark: |
| 20.x.x | 06.2024 | :white_check_mark: |
| 19.x.x | 05.2024 | :white_check_mark: |
Expand Down
3 changes: 2 additions & 1 deletion dataflow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 3e285ed

Please sign in to comment.