Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .fern/metadata.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"cliVersion": "0.113.1",
"generatorName": "fernapi/fern-typescript-sdk",
"generatorVersion": "3.28.6"
"generatorVersion": "3.28.11"
}
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import { FernAutopilotTestApiClient } from "";
const client = new FernAutopilotTestApiClient({ environment: "YOUR_BASE_URL" });
await client.imdb.createMovie({
title: "title",
rating: 1.1
rating: 1.1,
metadata: "metadata",
more_metadata: "more_metadata",
rank: 1
});
```

Expand Down
12 changes: 12 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## 2.1.0 - 2025-11-25
* feat: add additional movie metadata fields and upgrade generator
* The TypeScript SDK has been enhanced with new movie metadata fields and upgraded to the latest generator version for improved functionality.
* Key changes:
* Add metadata, more_metadata, and rank fields to movie creation and responses
* Remove deprecated User-Agent header and description field from Movie type
* Upgrade Fern TypeScript generator from 3.28.6 to 3.28.11
* Implement Headers class for improved HTTP header management
* Add comprehensive test utilities with custom header matching matchers
* Update all code examples and documentation to reflect new API structure
* 🌿 Generated with Fern

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "",
"version": "2.0.0",
"version": "2.1.0",
"private": false,
"repository": "github:fern-demo/autopilot-typescript-sdk",
"type": "commonjs",
Expand Down
266 changes: 133 additions & 133 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ Add a movie to the database
```typescript
await client.imdb.createMovie({
title: "title",
rating: 1.1
rating: 1.1,
metadata: "metadata",
more_metadata: "more_metadata",
rank: 1
});

```
Expand Down
4 changes: 2 additions & 2 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export class FernAutopilotTestApiClient {
{
"X-Fern-Language": "JavaScript",
"X-Fern-SDK-Name": "",
"X-Fern-SDK-Version": "2.0.0",
"User-Agent": "/2.0.0",
"X-Fern-SDK-Version": "2.1.0",
"User-Agent": "/2.1.0",
"X-Fern-Runtime": core.RUNTIME.type,
"X-Fern-Runtime-Version": core.RUNTIME.version,
},
Expand Down
5 changes: 4 additions & 1 deletion src/api/resources/imdb/client/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export class Imdb {
* @example
* await client.imdb.createMovie({
* title: "title",
* rating: 1.1
* rating: 1.1,
* metadata: "metadata",
* more_metadata: "more_metadata",
* rank: 1
* })
*/
public createMovie(
Expand Down
3 changes: 3 additions & 0 deletions src/api/resources/imdb/types/CreateMovieRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
export interface CreateMovieRequest {
title: string;
rating: number;
metadata: string;
more_metadata: string;
rank: number;
}
2 changes: 1 addition & 1 deletion src/api/resources/imdb/types/Movie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export interface Movie {
title: string;
/** The rating scale out of ten stars */
rating: number;
description: string;
metadata: string;
rank: number;
}
22 changes: 14 additions & 8 deletions src/core/fetcher/Fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getErrorResponseBody } from "./getErrorResponseBody.js";
import { getFetchFn } from "./getFetchFn.js";
import { getRequestBody } from "./getRequestBody.js";
import { getResponseBody } from "./getResponseBody.js";
import { Headers } from "./Headers.js";
import { makeRequest } from "./makeRequest.js";
import { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js";
import { requestWithRetries } from "./requestWithRetries.js";
Expand Down Expand Up @@ -77,9 +78,9 @@ const SENSITIVE_HEADERS = new Set([
"x-access-token",
]);

function redactHeaders(headers: Record<string, string>): Record<string, string> {
function redactHeaders(headers: Headers | Record<string, string>): Record<string, string> {
const filtered: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
for (const [key, value] of headers instanceof Headers ? headers.entries() : Object.entries(headers)) {
if (SENSITIVE_HEADERS.has(key.toLowerCase())) {
filtered[key] = "[REDACTED]";
} else {
Expand Down Expand Up @@ -207,10 +208,15 @@ function redactUrl(url: string): string {
return url.slice(0, queryStart + 1) + redactedParams.join("&") + url.slice(queryEnd);
}

async function getHeaders(args: Fetcher.Args): Promise<Record<string, string>> {
const newHeaders: Record<string, string> = {};
async function getHeaders(args: Fetcher.Args): Promise<Headers> {
const newHeaders: Headers = new Headers();

newHeaders.set(
"Accept",
args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*",
);
if (args.body !== undefined && args.contentType != null) {
newHeaders["Content-Type"] = args.contentType;
newHeaders.set("Content-Type", args.contentType);
}

if (args.headers == null) {
Expand All @@ -220,13 +226,13 @@ async function getHeaders(args: Fetcher.Args): Promise<Record<string, string>> {
for (const [key, value] of Object.entries(args.headers)) {
const result = await EndpointSupplier.get(value, { endpointMetadata: args.endpointMetadata ?? {} });
if (typeof result === "string") {
newHeaders[key] = result;
newHeaders.set(key, result);
continue;
}
if (result == null) {
continue;
}
newHeaders[key] = `${result}`;
newHeaders.set(key, `${result}`);
}
return newHeaders;
}
Expand Down Expand Up @@ -275,7 +281,7 @@ export async function fetcherImpl<R = unknown>(args: Fetcher.Args): Promise<APIR
method: args.method,
url: redactUrl(url),
statusCode: response.status,
responseHeaders: redactHeaders(Object.fromEntries(response.headers.entries())),
responseHeaders: redactHeaders(response.headers),
};
logger.debug("HTTP request succeeded", metadata);
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/fetcher/makeRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const makeRequest = async (
fetchFn: (url: string, init: RequestInit) => Promise<Response>,
url: string,
method: string,
headers: Record<string, string>,
headers: Headers | Record<string, string>,
requestBody: BodyInit | undefined,
timeoutMs?: number,
abortSignal?: AbortSignal,
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const SDK_VERSION = "2.0.0";
export const SDK_VERSION = "2.1.0";
80 changes: 80 additions & 0 deletions tests/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect } from "vitest";

interface CustomMatchers<R = unknown> {
toContainHeaders(expectedHeaders: Record<string, string>): R;
}

declare module "vitest" {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}

expect.extend({
toContainHeaders(actual: unknown, expectedHeaders: Record<string, string>) {
const isHeaders = actual instanceof Headers;
const isPlainObject = typeof actual === "object" && actual !== null && !Array.isArray(actual);

if (!isHeaders && !isPlainObject) {
throw new TypeError("Received value must be an instance of Headers or a plain object!");
}

if (typeof expectedHeaders !== "object" || expectedHeaders === null || Array.isArray(expectedHeaders)) {
throw new TypeError("Expected headers must be a plain object!");
}

const missingHeaders: string[] = [];
const mismatchedHeaders: Array<{ key: string; expected: string; actual: string | null }> = [];

for (const [key, value] of Object.entries(expectedHeaders)) {
let actualValue: string | null = null;

if (isHeaders) {
// Headers.get() is already case-insensitive
actualValue = (actual as Headers).get(key);
} else {
// For plain objects, do case-insensitive lookup
const actualObj = actual as Record<string, string>;
const lowerKey = key.toLowerCase();
const foundKey = Object.keys(actualObj).find((k) => k.toLowerCase() === lowerKey);
actualValue = foundKey ? actualObj[foundKey] : null;
}

if (actualValue === null || actualValue === undefined) {
missingHeaders.push(key);
} else if (actualValue !== value) {
mismatchedHeaders.push({ key, expected: value, actual: actualValue });
}
}

const pass = missingHeaders.length === 0 && mismatchedHeaders.length === 0;

const actualType = isHeaders ? "Headers" : "object";

if (pass) {
return {
message: () => `expected ${actualType} not to contain ${this.utils.printExpected(expectedHeaders)}`,
pass: true,
};
} else {
const messages: string[] = [];

if (missingHeaders.length > 0) {
messages.push(`Missing headers: ${this.utils.printExpected(missingHeaders.join(", "))}`);
}

if (mismatchedHeaders.length > 0) {
const mismatches = mismatchedHeaders.map(
({ key, expected, actual }) =>
`${key}: expected ${this.utils.printExpected(expected)} but got ${this.utils.printReceived(actual)}`,
);
messages.push(mismatches.join("\n"));
}

return {
message: () =>
`expected ${actualType} to contain ${this.utils.printExpected(expectedHeaders)}\n\n${messages.join("\n")}`,
pass: false,
};
}
},
});
12 changes: 6 additions & 6 deletions tests/unit/fetcher/Fetcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("Test fetcherImpl", () => {
"https://httpbin.org/post",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({ "X-Test": "x-test-header" }),
headers: expect.toContainHeaders({ "X-Test": "x-test-header" }),
body: JSON.stringify({ data: "test" }),
}),
);
Expand Down Expand Up @@ -66,7 +66,7 @@ describe("Test fetcherImpl", () => {
url,
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({ "X-Test": "x-test-header" }),
headers: expect.toContainHeaders({ "X-Test": "x-test-header" }),
body: expect.any(fs.ReadStream),
}),
);
Expand Down Expand Up @@ -102,7 +102,7 @@ describe("Test fetcherImpl", () => {
url,
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({ "X-Test": "x-test-header" }),
headers: expect.toContainHeaders({ "X-Test": "x-test-header" }),
}),
);
expect(result.ok).toBe(true);
Expand Down Expand Up @@ -148,7 +148,7 @@ describe("Test fetcherImpl", () => {
url,
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({ "X-Test": "x-test-header" }),
headers: expect.toContainHeaders({ "X-Test": "x-test-header" }),
}),
);
expect(result.ok).toBe(true);
Expand Down Expand Up @@ -194,7 +194,7 @@ describe("Test fetcherImpl", () => {
url,
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({ "X-Test": "x-test-header" }),
headers: expect.toContainHeaders({ "X-Test": "x-test-header" }),
}),
);
expect(result.ok).toBe(true);
Expand Down Expand Up @@ -238,7 +238,7 @@ describe("Test fetcherImpl", () => {
url,
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({ "X-Test": "x-test-header" }),
headers: expect.toContainHeaders({ "X-Test": "x-test-header" }),
}),
);
expect(result.ok).toBe(true);
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/fetcher/logging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("Fetcher Logging Integration", () => {
expect.objectContaining({
method: "POST",
url: "https://example.com/api",
headers: expect.objectContaining({
headers: expect.toContainHeaders({
"Content-Type": "application/json",
}),
hasBody: true,
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/fetcher/makeRequest.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Mock } from "vitest";
import { makeRequest } from "../../../src/core/fetcher/makeRequest";

describe("Test makeRequest", () => {
Expand All @@ -6,7 +7,7 @@ describe("Test makeRequest", () => {
const mockHeaders = { "Content-Type": "application/json" };
const mockBody = JSON.stringify({ key: "value" });

let mockFetch: import("vitest").Mock;
let mockFetch: Mock;

beforeEach(() => {
mockFetch = vi.fn();
Expand Down
Loading