Skip to content
Closed
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
91 changes: 91 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
"test": "bun test",
"test:unit": "bun test test/lib test/commands test/types",
"test:e2e": "bun test test/e2e",
"generate:client": "bun run script/api-client.ts",
"generate:skill": "bun run script/generate-skill.ts",
"check:skill": "bun run script/check-skill.ts"
},
"devDependencies": {
"@biomejs/biome": "2.3.8",
"@hey-api/openapi-ts": "0.91.1",
"@sentry/bun": "10.38.0",
"@sentry/esbuild-plugin": "^2.23.0",
"@sentry/node": "10.38.0",
Expand Down
7 changes: 7 additions & 0 deletions script/api-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createClient } from "@hey-api/openapi-ts";

createClient({
input:
"https://raw.githubusercontent.com/getsentry/sentry-api-schema/refs/heads/main/openapi-derefed.json",
output: "src/client",
});
16 changes: 16 additions & 0 deletions src/client/client.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This file is auto-generated by @hey-api/openapi-ts

import { type ClientOptions, type Config, createClient, createConfig } from './client';
import type { ClientOptions as ClientOptions2 } from './types.gen';

/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;

export const client = createClient(createConfig<ClientOptions2>());
288 changes: 288 additions & 0 deletions src/client/client/client.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
// This file is auto-generated by @hey-api/openapi-ts

import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
import { getValidRequestBody } from '../core/utils.gen';
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen';
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from './utils.gen';

type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};

export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);

const getConfig = (): Config => ({ ..._config });

const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};

const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();

const beforeRequest = async (options: RequestOptions) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
serializedBody: undefined,
};

if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}

if (opts.requestValidator) {
await opts.requestValidator(opts);
}

if (opts.body !== undefined && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body);
}

// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.serializedBody === '') {
opts.headers.delete('Content-Type');
}

const url = buildUrl(opts);

return { opts, url };
};

const request: Client['request'] = async (options) => {
// @ts-expect-error
const { opts, url } = await beforeRequest(options);
const requestInit: ReqInit = {
redirect: 'follow',
...opts,
body: getValidRequestBody(opts),
};

let request = new Request(url, requestInit);

for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}

// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response: Response;

try {
response = await _fetch(request);
} catch (error) {
// Handle fetch exceptions (AbortError, network errors, etc.)
let finalError = error;

for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, undefined as any, request, opts)) as unknown;
}
}

finalError = finalError || ({} as unknown);

if (opts.throwOnError) {
throw finalError;
}

// Return error response
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
request,
response: undefined as any,
};
}

for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts);
}
}

const result = {
request,
response,
};

if (response.ok) {
const parseAs =
(opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';

if (response.status === 204 || response.headers.get('Content-Length') === '0') {
let emptyData: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'text':
emptyData = await response[parseAs]();
break;
case 'formData':
emptyData = new FormData();
break;
case 'stream':
emptyData = response.body;
break;
case 'json':
default:
emptyData = {};
break;
}
return opts.responseStyle === 'data'
? emptyData
: {
data: emptyData,
...result,
};
}

let data: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'text':
data = await response[parseAs]();
break;
case 'json': {
// Some servers return 200 with no Content-Length and empty body.
// response.json() would throw; read as text and parse if non-empty.
const text = await response.text();
data = text ? JSON.parse(text) : {};
break;
}
case 'stream':
return opts.responseStyle === 'data'
? response.body
: {
data: response.body,
...result,
};
}

if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}

if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}

return opts.responseStyle === 'data'
? data
: {
data,
...result,
};
}

const textError = await response.text();
let jsonError: unknown;

try {
jsonError = JSON.parse(textError);
} catch {
// noop
}

const error = jsonError ?? textError;
let finalError = error;

for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}

finalError = finalError || ({} as string);

if (opts.throwOnError) {
throw finalError;
}

// TODO: we probably want to return error and improve types
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
...result,
};
};

const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
request({ ...options, method });

const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options);
return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
headers: opts.headers as unknown as Record<string, string>,
method,
onRequest: async (url, init) => {
let request = new Request(url, init);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
return request;
},
serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
url,
});
};

return {
buildUrl,
connect: makeMethodFn('CONNECT'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),
getConfig,
head: makeMethodFn('HEAD'),
interceptors,
options: makeMethodFn('OPTIONS'),
patch: makeMethodFn('PATCH'),
post: makeMethodFn('POST'),
put: makeMethodFn('PUT'),
request,
setConfig,
sse: {
connect: makeSseFn('CONNECT'),
delete: makeSseFn('DELETE'),
get: makeSseFn('GET'),
head: makeSseFn('HEAD'),
options: makeSseFn('OPTIONS'),
patch: makeSseFn('PATCH'),
post: makeSseFn('POST'),
put: makeSseFn('PUT'),
trace: makeSseFn('TRACE'),
},
trace: makeMethodFn('TRACE'),
} as Client;
};
25 changes: 25 additions & 0 deletions src/client/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// This file is auto-generated by @hey-api/openapi-ts

export type { Auth } from "../core/auth.gen";
export type { QuerySerializerOptions } from "../core/bodySerializer.gen";
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from "../core/bodySerializer.gen";
export { buildClientParams } from "../core/params.gen";
export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen";
export { createClient } from "./client.gen";
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
RequestOptions,
RequestResult,
ResolvedRequestOptions,
ResponseStyle,
TDataShape,
} from "./types.gen";
export { createConfig, mergeHeaders } from "./utils.gen";
Loading
Loading