Skip to content

Commit

Permalink
Merge branch 'master' into client-default-implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinTail authored Jan 24, 2025
2 parents 016607d + aa2a2f6 commit 5fed670
Show file tree
Hide file tree
Showing 18 changed files with 558 additions and 440 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, make-v22 ]
branches: [ master, v19, v20, v21 ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master, v19, v20, v21, make-v22 ]
branches: [ master, v19, v20, v21 ]
schedule:
- cron: '26 8 * * 1'

Expand Down
54 changes: 54 additions & 0 deletions .github/workflows/headers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Headers update

on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 0" # Runs every Sunday at midnight UTC

jobs:
run-bash-and-pr:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- uses: fregante/setup-git-user@v2

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: yarn install

- name: Check for new headers on IANA.ORG
run: yarn tsx tools/headers.ts

- name: Check for changes
id: git-state
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changes=true" >> $GITHUB_OUTPUT
else
echo "changes=false" >> $GITHUB_OUTPUT
fi
- name: Create branch, commit, and push changes
if: steps.git-state.outputs.changes == 'true'
run: |
BRANCH_NAME="headers-update-$(date +%Y%m%d)"
git checkout -b $BRANCH_NAME
git add .
git commit -m "Changed well-known headers on $(date +%Y-%m-%d)"
git push origin $BRANCH_NAME
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Create a pull request
if: steps.git-state.outputs.changes == 'true'
uses: peter-evans/create-pull-request@v5
with:
base: "master"
branch: ${{ steps.create-branch.outputs.branch-name }}
title: "Well-known headers update"
body: "This PR contains automated updates generated by the weekly workflow."
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, make-v22 ]
branches: [ master, v19, v20, v21 ]
pull_request:
branches: [ master, v19, v20, v21, make-v22 ]
branches: [ master, 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, make-v22 ]
branches: [ master, v19, v20, v21 ]
pull_request:
branches: [ master, v19, v20, v21, make-v22 ]
branches: [ master, v19, v20, v21 ]


jobs:
Expand Down
24 changes: 21 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@

### v22.0.0

- Minimum supported Node versions: 20.9.0 and 22.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.
- `BuiltinLogger::profile()` behavior changed for picoseconds: expressing them through nanoseconds;
- Changes to client generated by `Integration`:
- The `splitResponse` property on the constructor argument is removed;
- Feature: handling all headers as 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;
- The generated `Documentation` recognizes both `x-` prefixed inputs and well-known headers listed on IANA.ORG;
- You can customize that behavior by using the new option `isHeader` of the `Documentation::constructor()`.
- The `splitResponse` property on the `Integration::constructor()` argument is removed;
- Changes to the client code generated by `Integration`:
- The class name changed from `ExpressZodAPIClient` to just `Client`;
- 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;
Expand All @@ -17,6 +24,7 @@
- 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);
- Tags should be declared as the keys of the augmented interface `TagOverrides` instead;
- The public method `Endpoint::getSecurity()` now returns an array;
- Consider the automated migration using the built-in ESLint rule.

```js
Expand All @@ -33,10 +41,15 @@ export default [
```diff
createConfig({
- tags: {},
inputSources: {
- get: ["query", "headers"] // if you have headers on last place
+ get: ["headers", "query"] // move headers to avoid overwrites
}
});

new Documentation({
+ tags: {},
+ isHeader: (name, method, path) => {} // optional
});

new EndpointsFactory(
Expand Down Expand Up @@ -74,6 +87,11 @@ new Documentation({

## Version 21

### v21.11.1

- Common styling methods (coloring) are extracted from the built-in logger instance:
- This measure is to reduce memory consumption when using a child logger.

### v21.11.0

- New public property `ctx` is available on instances of `BuiltinLogger`:
Expand Down
85 changes: 42 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,26 @@ Start your API server with I/O schema validation and custom middlewares in minut
13. [Enabling compression](#enabling-compression)
5. [Advanced features](#advanced-features)
1. [Customizing input sources](#customizing-input-sources)
2. [Nested routes](#nested-routes)
3. [Route path params](#route-path-params)
4. [Multiple schemas for one route](#multiple-schemas-for-one-route)
5. [Response customization](#response-customization)
6. [Empty response](#empty-response)
7. [Error handling](#error-handling)
8. [Production mode](#production-mode)
9. [Non-object response](#non-object-response) including file downloads
10. [File uploads](#file-uploads)
11. [Serving static files](#serving-static-files)
12. [Connect to your own express app](#connect-to-your-own-express-app)
13. [Testing endpoints](#testing-endpoints)
14. [Testing middlewares](#testing-middlewares)
2. [Headers as input source](#headers-as-input-source)
3. [Nested routes](#nested-routes)
4. [Route path params](#route-path-params)
5. [Multiple schemas for one route](#multiple-schemas-for-one-route)
6. [Response customization](#response-customization)
7. [Empty response](#empty-response)
8. [Error handling](#error-handling)
9. [Production mode](#production-mode)
10. [Non-object response](#non-object-response) including file downloads
11. [File uploads](#file-uploads)
12. [Serving static files](#serving-static-files)
13. [Connect to your own express app](#connect-to-your-own-express-app)
14. [Testing endpoints](#testing-endpoints)
15. [Testing middlewares](#testing-middlewares)
6. [Special needs](#special-needs)
1. [Different responses for different status codes](#different-responses-for-different-status-codes)
2. [Array response](#array-response) for migrating legacy APIs
3. [Headers as input source](#headers-as-input-source)
4. [Accepting raw data](#accepting-raw-data)
5. [Graceful shutdown](#graceful-shutdown)
6. [Subscriptions](#subscriptions)
3. [Accepting raw data](#accepting-raw-data)
4. [Graceful shutdown](#graceful-shutdown)
5. [Subscriptions](#subscriptions)
7. [Integration and Documentation](#integration-and-documentation)
1. [Zod Plugin](#zod-plugin)
2. [Generating a Frontend Client](#generating-a-frontend-client)
Expand Down Expand Up @@ -726,6 +726,31 @@ createConfig({
});
```

## Headers as input source

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;
- the request headers acquired that way are always lowercase when describing their validation schemas.

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

createConfig({
inputSources: {
get: ["headers", "query"], // headers have lowest priority
}, // ...
});

defaultEndpointsFactory.build({
input: z.object({
"x-request-id": z.string(), // this one is from request.headers
id: z.string(), // this one is from request.query
}), // ...
});
```

## Nested routes

Suppose you want to assign both `/v1/path` and `/v1/path/subpath` routes with Endpoints:
Expand Down Expand Up @@ -1128,32 +1153,6 @@ The `arrayResultHandler` expects your endpoint to have `items` property in the `
assigned to that property is used as the response. This approach also supports examples, as well as documentation and
client generation. Check out [the example endpoint](/example/endpoints/list-users.ts) for more details.

## Headers as input source

In a similar way you can enable the inclusion of request headers into the input sources. This is an opt-in feature.
Please note:

- only the custom headers (the ones having `x-` prefix) will be combined into the `input`,
- the request headers acquired that way are lowercase when describing their validation schemas.

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

createConfig({
inputSources: {
get: ["query", "headers"],
}, // ...
});

defaultEndpointsFactory.build({
input: z.object({
"x-request-id": z.string(), // this one is from request.headers
id: z.string(), // this one is from request.query
}), // ...
});
```

## Accepting raw data

Some APIs may require an endpoint to be able to accept and process raw data, such as streaming or uploading a binary
Expand Down
3 changes: 1 addition & 2 deletions dataflow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
"@types/semver": "^7.5.8",
"@types/swagger-ui-express": "^4.1.6",
"@typescript-eslint/rule-tester": "^8.15.0",
"@vitest/coverage-v8": "^3.0.1",
"@vitest/coverage-v8": "^3.0.3",
"camelize-ts": "^3.0.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
Expand All @@ -154,7 +154,7 @@
"typescript": "^5.5.2",
"typescript-eslint": "^8.15.0",
"undici": "^6.19.8",
"vitest": "^3.0.1",
"vitest": "^3.0.3",
"zod": "^3.23.0"
},
"resolutions": {
Expand Down
12 changes: 2 additions & 10 deletions src/common-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request } from "express";
import { chain, memoizeWith, pickBy, xprod } from "ramda";
import { chain, memoizeWith, xprod } from "ramda";
import { z } from "zod";
import { CommonConfig, InputSource, InputSources } from "./config-type";
import { contentTypes } from "./content-type";
Expand Down Expand Up @@ -42,13 +42,6 @@ const fallbackInputSource: InputSource[] = ["body", "query", "params"];
export const getActualMethod = (request: Request) =>
request.method.toLowerCase() as Method | AuxMethod;

export const isCustomHeader = (name: string): name is `x-${string}` =>
name.startsWith("x-");

/** @see https://nodejs.org/api/http.html#messageheaders */
export const getCustomHeaders = (headers: FlatObject): FlatObject =>
pickBy((_, key) => isCustomHeader(key), headers); // twice faster than flip()

export const getInput = (
req: Request,
userDefined: CommonConfig["inputSources"] = {},
Expand All @@ -61,8 +54,7 @@ export const getInput = (
fallbackInputSource
)
.filter((src) => (src === "files" ? areFilesAvailable(req) : true))
.map((src) => (src === "headers" ? getCustomHeaders(req[src]) : req[src]))
.reduce<FlatObject>((agg, obj) => Object.assign(agg, obj), {});
.reduce<FlatObject>((agg, src) => Object.assign(agg, req[src]), {});
};

export const ensureError = (subject: unknown): Error =>
Expand Down
Loading

0 comments on commit 5fed670

Please sign in to comment.