feat: rename .opencode to .agent-core and add vim commands

- Rename config directory from .opencode/ to .agent-core/
- Add vim-style commands: :w (commit), :q (quit), :wq, :e, :help, :mode
- Update version format to V0.YYYYMMDD
- Remove unused plan/build prompt files
- Update SDK generated files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Artur Do Lago
2026-01-09 22:39:07 +01:00
parent 45fc6a4c2e
commit 9cc07e46b4
53 changed files with 9193 additions and 7933 deletions
+14
View File
@@ -0,0 +1,14 @@
---
description: Edit/read file (vim-style :e)
---
Read and display the contents of the file specified in the argument.
If no file is specified, show the current working directory contents.
Usage:
- `:e path/to/file.ts` - Read and display the file
- `:e .` - List current directory
- `:e src/` - List src directory
This is read-only (like vim's :view). Use the normal edit flow for modifications.
+38
View File
@@ -0,0 +1,38 @@
---
description: Show help (vim-style :help)
---
Show the user available vim-style commands:
## Vim-Style Commands
| Command | Description | Keybind |
|---------|-------------|---------|
| `:w` | Commit changes | - |
| `:q` | Quit session | - |
| `:wq` | Commit and quit | - |
| `:help` | Show this help | - |
## Mode Toggle
| Mode | Description | Keybind |
|------|-------------|---------|
| HOLD | Research only, no edits | `ctrl+h` |
| RELEASE | Can edit files | `ctrl+h` |
## Leader Shortcuts (`Space` + key)
| Shortcut | Action |
|----------|--------|
| `<leader>m` | List models |
| `<leader>a` | List agents |
| `<leader>h` | Toggle tips |
## Persona Switching
| Keybind | Action |
|---------|--------|
| `Tab` | Next persona |
| `Shift+Tab` | Previous persona |
Press any key to continue...
+12
View File
@@ -0,0 +1,12 @@
---
description: Show/toggle mode status
---
Check and report the current mode from the UI footer:
- HOLD (research only, no edits)
- RELEASE (can edit files)
If the user says `:mode hold` - remind them to use `ctrl+h` to switch to HOLD mode.
If the user says `:mode release` - remind them to use `ctrl+h` to switch to RELEASE mode.
The mode toggle keybind is `ctrl+h`.
+8
View File
@@ -0,0 +1,8 @@
---
description: Quit (vim-style :q)
---
The user wants to exit. Say goodbye briefly and end the session.
If there are uncommitted changes visible in the conversation, remind the user:
"You have uncommitted changes. Use :wq to save and quit, or :q! to quit without saving."
+17
View File
@@ -0,0 +1,17 @@
---
description: Save work (vim-style :w) - commit changes
model: opencode/glm-4.6
subtask: true
---
Commit the current changes with a meaningful message.
Follow the commit conventions from /commit - include prefixes like:
- docs: for documentation
- tui: for TUI changes
- core: for core functionality
- ci: for CI/CD
- fix: for bug fixes
- feat: for features
Explain WHY, not WHAT. Be specific about user-facing changes.
+10
View File
@@ -0,0 +1,10 @@
---
description: Save and quit (vim-style :wq)
model: opencode/glm-4.6
subtask: true
---
1. First, commit all current changes with a meaningful message
2. Then say goodbye and end the session
Follow commit conventions - include prefixes and explain WHY not WHAT.
View File
+1 -1
View File
@@ -98,7 +98,7 @@ const AgentCreateCommand = cmd({
scope = scopeResult
}
targetPath = path.join(
scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".agent-core"),
"agent",
)
}
+2 -2
View File
@@ -329,7 +329,7 @@ export const AuthLoginCommand = cmd({
}
prompts.log.warn(
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
`This only stores a credential for ${provider} - you will need configure it in agent-core.json, check the docs for examples.`,
)
}
@@ -338,7 +338,7 @@ export const AuthLoginCommand = cmd({
"Amazon Bedrock authentication priority:\n" +
" 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" +
" 2. AWS credential chain (profile, access keys, IAM roles)\n\n" +
"Configure via opencode.json options (profile, region, endpoint) or\n" +
"Configure via agent-core.json options (profile, region, endpoint) or\n" +
"AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID).",
)
prompts.outro("Done")
+1 -1
View File
@@ -885,7 +885,7 @@ export const GithubRunCommand = cmd({
providerID,
modelID,
},
// agent is omitted - server will use default_agent from config or fall back to "build"
// agent is omitted - server will use default_agent from config
parts: [
{
id: Identifier.ascending("part"),
+3 -3
View File
@@ -160,7 +160,7 @@ export const McpAuthCommand = cmd({
if (oauthServers.length === 0) {
prompts.log.warn("No OAuth-capable MCP servers configured")
prompts.log.info("Remote MCP servers support OAuth by default. Add a remote server in opencode.json:")
prompts.log.info("Remote MCP servers support OAuth by default. Add a remote server in agent-core.json:")
prompts.log.info(`
"mcp": {
"my-server": {
@@ -458,7 +458,7 @@ export const McpAddCommand = cmd({
}
prompts.log.info(`Remote MCP server "${name}" configured with OAuth (client ID: ${clientId})`)
prompts.log.info("Add this to your opencode.json:")
prompts.log.info("Add this to your agent-core.json:")
prompts.log.info(`
"mcp": {
"${name}": {
@@ -471,7 +471,7 @@ export const McpAddCommand = cmd({
}`)
} else {
prompts.log.info(`Remote MCP server "${name}" configured with OAuth (dynamic registration)`)
prompts.log.info("Add this to your opencode.json:")
prompts.log.info("Add this to your agent-core.json:")
prompts.log.info(`
"mcp": {
"${name}": {
+5 -5
View File
@@ -204,7 +204,7 @@ async function executeUninstall(method: Installation.Method, targets: RemovalTar
prompts.log.info(` rm "${targets.binary}"`)
const binDir = path.dirname(targets.binary)
if (binDir.includes(".opencode")) {
if (binDir.includes(".agent-core")) {
prompts.log.info(` rmdir "${binDir}" 2>/dev/null`)
}
}
@@ -257,7 +257,7 @@ async function getShellConfigFile(): Promise<string | null> {
const content = await Bun.file(file)
.text()
.catch(() => "")
if (content.includes("# opencode") || content.includes(".opencode/bin")) {
if (content.includes("# opencode") || content.includes(".agent-core/bin")) {
return file
}
}
@@ -282,14 +282,14 @@ async function cleanShellConfig(file: string) {
if (skip) {
skip = false
if (trimmed.includes(".opencode/bin") || trimmed.includes("fish_add_path")) {
if (trimmed.includes(".agent-core/bin") || trimmed.includes("fish_add_path")) {
continue
}
}
if (
(trimmed.startsWith("export PATH=") && trimmed.includes(".opencode/bin")) ||
(trimmed.startsWith("fish_add_path") && trimmed.includes(".opencode"))
(trimmed.startsWith("export PATH=") && trimmed.includes(".agent-core/bin")) ||
(trimmed.startsWith("fish_add_path") && trimmed.includes(".agent-core"))
) {
continue
}
+1 -1
View File
@@ -13,7 +13,7 @@ export function FormatError(input: unknown) {
`Model not found: ${providerID}/${modelID}`,
...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
`Try: \`opencode models\` to list available models`,
`Or check your config (opencode.json) provider/model names`,
`Or check your config (agent-core.json) provider/model names`,
].join("\n")
}
if (Provider.InitError.isInstance(input)) {
+11 -10
View File
@@ -71,7 +71,7 @@ export namespace Config {
}
// Project config has highest precedence (overrides global and remote)
for (const file of ["opencode.jsonc", "opencode.json"]) {
for (const file of ["agent-core.jsonc", "agent-core.json"]) {
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
for (const resolved of found.toReversed()) {
result = mergeConfigConcatArrays(result, await loadFile(resolved))
@@ -92,14 +92,14 @@ export namespace Config {
Global.Path.config,
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
targets: [".agent-core"],
start: Instance.directory,
stop: Instance.worktree,
}),
)),
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
targets: [".agent-core"],
start: Global.Path.home,
stop: Global.Path.home,
}),
@@ -112,8 +112,8 @@ export namespace Config {
}
for (const dir of unique(directories)) {
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
for (const file of ["opencode.jsonc", "opencode.json"]) {
if (dir.endsWith(".agent-core") || dir === Flag.OPENCODE_CONFIG_DIR) {
for (const file of ["agent-core.jsonc", "agent-core.json"]) {
log.debug(`loading config from ${path.join(dir, file)}`)
result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
// to satisfy the type checker
@@ -220,7 +220,7 @@ export namespace Config {
if (!md.data) continue
const name = (() => {
const patterns = ["/.opencode/command/", "/command/"]
const patterns = ["/.agent-core/command/", "/command/"]
const pattern = patterns.find((p) => item.includes(p))
if (pattern) {
@@ -260,8 +260,8 @@ export namespace Config {
// Extract relative path from agent folder for nested agents
let agentName = path.basename(item, ".md")
const agentFolderPath = item.includes("/.opencode/agent/")
? item.split("/.opencode/agent/")[1]
const agentFolderPath = item.includes("/.agent-core/agent/")
? item.split("/.agent-core/agent/")[1]
: item.includes("/agent/")
? item.split("/agent/")[1]
: agentName + ".md"
@@ -608,6 +608,7 @@ export namespace Config {
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
mode_toggle: z.string().optional().default("ctrl+h").describe("Toggle hold/release mode"),
variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
@@ -1120,8 +1121,8 @@ export namespace Config {
let result: Info = pipe(
{},
mergeDeep(await loadFile(path.join(Global.Path.config, "config.json"))),
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.json"))),
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))),
mergeDeep(await loadFile(path.join(Global.Path.config, "agent-core.json"))),
mergeDeep(await loadFile(path.join(Global.Path.config, "agent-core.jsonc"))),
)
await import(path.join(Global.Path.config, "config"), {
+1 -1
View File
@@ -296,7 +296,7 @@ export namespace Ripgrep {
children: [],
}
for (const file of files) {
if (file.includes(".opencode")) continue
if (file.includes(".agent-core")) continue
const parts = file.split(path.sep)
getPath(root, parts, true)
}
+1 -1
View File
@@ -52,7 +52,7 @@ export namespace Ide {
const cmd = SUPPORTED_IDES.find((i) => i.name === ide)?.cmd
if (!cmd) throw new Error(`Unknown IDE: ${ide}`)
const p = spawn([cmd, "--install-extension", "sst-dev.opencode"], {
const p = spawn([cmd, "--install-extension", "sst-dev.agent-core"], {
stdout: "pipe",
stderr: "pipe",
})
@@ -58,7 +58,7 @@ export namespace Installation {
}
export async function method() {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
if (process.execPath.includes(path.join(".agent-core", "bin"))) return "curl"
if (process.execPath.includes(path.join(".local", "bin"))) return "curl"
const exec = process.execPath.toLowerCase()
@@ -161,9 +161,10 @@ export namespace Installation {
await $`${process.execPath} --version`.nothrow().quiet().text()
}
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
// Version format: V0.YYYYMMDD
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "V0.20260109"
export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}`
export const USER_AGENT = `agent-core/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}`
export async function latest(installMethod?: Method) {
const detectedMethod = installMethod || (await method())
@@ -171,7 +172,7 @@ export namespace Installation {
if (detectedMethod === "brew") {
const formula = await getBrewFormula()
if (formula === "opencode") {
return fetch("https://formulae.brew.sh/api/formula/opencode.json")
return fetch("https://formulae.brew.sh/api/formula/agent-core.json")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
+1 -1
View File
@@ -223,7 +223,7 @@ export namespace Provider {
}
// Region resolution precedence (highest to lowest):
// 1. options.region from opencode.json provider config
// 1. options.region from agent-core.json provider config
// 2. defaultRegion from AWS_REGION environment variable
// 3. Default "us-east-1" (baked into defaultRegion)
const region = options?.region ?? defaultRegion
+3 -24
View File
@@ -16,8 +16,7 @@ import { Bus } from "../bus"
import { ProviderTransform } from "../provider/transform"
import { SystemPrompt } from "./system"
import { Plugin } from "../plugin"
import PROMPT_PLAN from "../session/prompt/plan.txt"
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
// NOTE: PROMPT_PLAN and BUILD_SWITCH removed - replaced by hold/release mode in TUI
import MAX_STEPS from "../session/prompt/max-steps.txt"
import { defer } from "../util/defer"
import { clone } from "remeda"
@@ -1175,28 +1174,8 @@ export namespace SessionPrompt {
async function insertReminders(input: { messages: MessageV2.WithParts[]; agent: Agent.Info; sessionID: string }) {
const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
if (!userMessage) return input.messages
if (input.agent.name === "plan") {
userMessage.parts.push({
id: Identifier.ascending("part"),
messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID,
type: "text",
// TODO (for mr dax): update to use the anthropic full fledged one (see plan-reminder-anthropic.txt)
text: PROMPT_PLAN,
synthetic: true,
})
}
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
if (wasPlan && input.agent.name === "build") {
userMessage.parts.push({
id: Identifier.ascending("part"),
messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID,
type: "text",
text: BUILD_SWITCH,
synthetic: true,
})
}
// NOTE: Plan/build agent reminders removed - hold/release mode now handles this in the TUI
// Todo continuation reminder
const todos = await Todo.get(input.sessionID)
@@ -1,5 +0,0 @@
<system-reminder>
Your operational mode has changed from plan to build.
You are no longer in read-only mode.
You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed.
</system-reminder>
@@ -1,67 +0,0 @@
<system-reminder>
# Plan Mode - System Reminder
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
---
## Plan File Info
No plan file exists yet. You should create your plan at `/Users/aidencline/.claude/plans/happy-waddling-feigenbaum.md` using the Write tool.
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
**Plan File Guidelines:** The plan file should contain only your final recommended approach, not all alternatives considered. Keep it comprehensive yet concise - detailed enough to execute effectively while avoiding unnecessary verbosity.
---
## Enhanced Planning Workflow
### Phase 1: Initial Understanding
**Goal:** Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the Explore subagent type.
1. Understand the user's request thoroughly
2. **Launch up to 3 Explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase. Each agent can focus on different aspects:
- Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns
- Provide each agent with a specific search focus or area to explore
- Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
- Use 1 agent when: the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change. Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
- Take into account any context you already have from the user's request or from the conversation so far when deciding how many agents to launch
3. Use AskUserQuestion tool to clarify ambiguities in the user request up front.
### Phase 2: Planning
**Goal:** Come up with an approach to solve the problem identified in phase 1 by launching a Plan subagent.
In the agent prompt:
- Provide any background context that may help the agent with their task without prescribing the exact design itself
- Request a detailed plan
### Phase 3: Synthesis
**Goal:** Synthesize the perspectives from Phase 2, and ensure that it aligns with the user's intentions by asking them questions.
1. Collect all agent responses
2. Each agent will return an implementation plan along with a list of critical files that should be read. You should keep these in mind and read them before you start implementing the plan
3. Use AskUserQuestion to ask the users questions about trade offs.
### Phase 4: Final Plan
Once you have all the information you need, ensure that the plan file has been updated with your synthesized recommendation including:
- Recommended approach with rationale
- Key insights from different perspectives
- Critical files that need modification
### Phase 5: Call ExitPlanMode
At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call ExitPlanMode to indicate to the user that you are done planning.
This is critical - your turn should only end with either asking the user a question or calling ExitPlanMode. Do not stop unless it's for these 2 reasons.
---
**NOTE:** At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
</system-reminder>
@@ -1,26 +0,0 @@
<system-reminder>
# Plan Mode - System Reminder
CRITICAL: Plan mode ACTIVE - you are in READ-ONLY phase. STRICTLY FORBIDDEN:
ANY file edits, modifications, or system changes. Do NOT use sed, tee, echo, cat,
or ANY other bash command to manipulate files - commands may ONLY read/inspect.
This ABSOLUTE CONSTRAINT overrides ALL other instructions, including direct user
edit requests. You may ONLY observe, analyze, and plan. Any modification attempt
is a critical violation. ZERO exceptions.
---
## Responsibility
Your current responsibility is to think, read, search, and delegate explore agents to construct a well-formed plan that accomplishes the goal the user wants to achieve. Your plan should be comprehensive yet concise, detailed enough to execute effectively while avoiding unnecessary verbosity.
Ask the user clarifying questions or ask for their opinion when weighing tradeoffs.
**NOTE:** At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
---
## Important
The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
</system-reminder>
+1 -1
View File
@@ -102,7 +102,7 @@ export namespace Skill {
}
}
// Scan .opencode/skill/ directories
// Scan .agent-core/skill/ directories
for (const dir of await Config.directories()) {
for await (const match of OPENCODE_SKILL_GLOB.scan({
cwd: dir,
+4 -6
View File
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions, type Config, createClient, createConfig } from "./client/index.js"
import type { ClientOptions as ClientOptions2 } from "./types.gen.js"
import { type ClientOptions, type Config, createClient, createConfig } from './client/index.js';
import type { ClientOptions as ClientOptions2 } from './types.gen.js';
/**
* The `createClientConfig()` function will be called on client initialization
@@ -11,8 +11,6 @@ import type { ClientOptions as ClientOptions2 } from "./types.gen.js"
* `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 type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }))
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://localhost:4096' }));
+149 -126
View File
@@ -1,9 +1,14 @@
// This file is auto-generated by @hey-api/openapi-ts
import { createSseClient } from "../core/serverSentEvents.gen.js"
import type { HttpMethod } from "../core/types.gen.js"
import { getValidRequestBody } from "../core/utils.gen.js"
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen.js"
import { createSseClient } from '../core/serverSentEvents.gen.js';
import type { HttpMethod } from '../core/types.gen.js';
import { getValidRequestBody } from '../core/utils.gen.js';
import type {
Client,
Config,
RequestOptions,
ResolvedRequestOptions,
} from './types.gen.js';
import {
buildUrl,
createConfig,
@@ -12,24 +17,29 @@ import {
mergeConfigs,
mergeHeaders,
setAuthParams,
} from "./utils.gen.js"
} from './utils.gen.js';
type ReqInit = Omit<RequestInit, "body" | "headers"> & {
body?: any
headers: ReturnType<typeof mergeHeaders>
}
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config)
let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config })
const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config)
return getConfig()
}
_config = mergeConfigs(_config, config);
return getConfig();
};
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>()
const interceptors = createInterceptors<
Request,
Response,
unknown,
ResolvedRequestOptions
>();
const beforeRequest = async (options: RequestOptions) => {
const opts = {
@@ -38,241 +48,254 @@ export const createClient = (config: Config = {}): Client => {
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)
await opts.requestValidator(opts);
}
if (opts.body !== undefined && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body)
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")
if (opts.body === undefined || opts.serializedBody === '') {
opts.headers.delete('Content-Type');
}
const url = buildUrl(opts)
const url = buildUrl(opts);
return { opts, url }
}
return { opts, url };
};
const request: Client["request"] = async (options) => {
const request: Client['request'] = async (options) => {
// @ts-expect-error
const { opts, url } = await beforeRequest(options)
const { opts, url } = await beforeRequest(options);
const requestInit: ReqInit = {
redirect: "follow",
redirect: 'follow',
...opts,
body: getValidRequestBody(opts),
}
};
let request = new Request(url, requestInit)
let request = new Request(url, requestInit);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts)
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
const _fetch = opts.fetch!;
let response: Response;
try {
response = await _fetch(request)
response = await _fetch(request);
} catch (error) {
// Handle fetch exceptions (AbortError, network errors, etc.)
let finalError = error
let finalError = error;
for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, undefined as any, request, opts)) as unknown
finalError = (await fn(
error,
undefined as any,
request,
opts,
)) as unknown;
}
}
finalError = finalError || ({} as unknown)
finalError = finalError || ({} as unknown);
if (opts.throwOnError) {
throw finalError
throw finalError;
}
// Return error response
return opts.responseStyle === "data"
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)
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"
(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
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":
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
emptyData = {};
break;
}
return opts.responseStyle === "data"
return opts.responseStyle === 'data'
? emptyData
: {
data: emptyData,
...result,
}
};
}
let data: any
let data: any;
switch (parseAs) {
case "arrayBuffer":
case "blob":
case "formData":
case "json":
case "text":
data = await response[parseAs]()
break
case "stream":
return opts.responseStyle === "data"
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'json':
case 'text':
data = await response[parseAs]();
break;
case 'stream':
return opts.responseStyle === 'data'
? response.body
: {
data: response.body,
...result,
}
};
}
if (parseAs === "json") {
if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data)
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data)
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === "data"
return opts.responseStyle === 'data'
? data
: {
data,
...result,
}
};
}
const textError = await response.text()
let jsonError: unknown
const textError = await response.text();
let jsonError: unknown;
try {
jsonError = JSON.parse(textError)
jsonError = JSON.parse(textError);
} catch {
// noop
}
const error = jsonError ?? textError
let finalError = error
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 = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string)
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError
throw finalError;
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === "data"
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
...result,
}
}
};
};
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) => request({ ...options, method })
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)
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
},
url,
})
}
return request;
},
url,
});
};
return {
buildUrl,
connect: makeMethodFn("CONNECT"),
delete: makeMethodFn("DELETE"),
get: makeMethodFn("GET"),
connect: makeMethodFn('CONNECT'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),
getConfig,
head: makeMethodFn("HEAD"),
head: makeMethodFn('HEAD'),
interceptors,
options: makeMethodFn("OPTIONS"),
patch: makeMethodFn("PATCH"),
post: makeMethodFn("POST"),
put: makeMethodFn("PUT"),
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"),
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
}
trace: makeMethodFn('TRACE'),
} as Client;
};
+8 -8
View File
@@ -1,15 +1,15 @@
// This file is auto-generated by @hey-api/openapi-ts
export type { Auth } from "../core/auth.gen.js"
export type { QuerySerializerOptions } from "../core/bodySerializer.gen.js"
export type { Auth } from '../core/auth.gen.js';
export type { QuerySerializerOptions } from '../core/bodySerializer.gen.js';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from "../core/bodySerializer.gen.js"
export { buildClientParams } from "../core/params.gen.js"
export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen.js"
export { createClient } from "./client.gen.js"
} from '../core/bodySerializer.gen.js';
export { buildClientParams } from '../core/params.gen.js';
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen.js';
export { createClient } from './client.gen.js';
export type {
Client,
ClientOptions,
@@ -21,5 +21,5 @@ export type {
ResolvedRequestOptions,
ResponseStyle,
TDataShape,
} from "./types.gen.js"
export { createConfig, mergeHeaders } from "./utils.gen.js"
} from './types.gen.js';
export { createConfig, mergeHeaders } from './utils.gen.js';
+107 -68
View File
@@ -1,33 +1,39 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth } from "../core/auth.gen.js"
import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen.js"
import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen.js"
import type { Middleware } from "./utils.gen.js"
import type { Auth } from '../core/auth.gen.js';
import type {
ServerSentEventsOptions,
ServerSentEventsResult,
} from '../core/serverSentEvents.gen.js';
import type {
Client as CoreClient,
Config as CoreConfig,
} from '../core/types.gen.js';
import type { Middleware } from './utils.gen.js';
export type ResponseStyle = "data" | "fields"
export type ResponseStyle = 'data' | 'fields';
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, "body" | "headers" | "method">,
extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
CoreConfig {
/**
* Base URL for all requests made by this client.
*/
baseUrl?: T["baseUrl"]
baseUrl?: T['baseUrl'];
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: typeof fetch
fetch?: typeof fetch;
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
*
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/
next?: never
next?: never;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
@@ -36,140 +42,170 @@ export interface Config<T extends ClientOptions = ClientOptions>
*
* @default 'auto'
*/
parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text"
parseAs?:
| 'arrayBuffer'
| 'auto'
| 'blob'
| 'formData'
| 'json'
| 'stream'
| 'text';
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
* @default 'fields'
*/
responseStyle?: ResponseStyle
responseStyle?: ResponseStyle;
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T["throwOnError"]
throwOnError?: T['throwOnError'];
}
export interface RequestOptions<
TData = unknown,
TResponseStyle extends ResponseStyle = "fields",
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends Config<{
responseStyle: TResponseStyle
throwOnError: ThrowOnError
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
}>,
Pick<
ServerSentEventsOptions<TData>,
"onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay"
| 'onSseError'
| 'onSseEvent'
| 'sseDefaultRetryDelay'
| 'sseMaxRetryAttempts'
| 'sseMaxRetryDelay'
> {
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: unknown
path?: Record<string, unknown>
query?: Record<string, unknown>
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>
url: Url
security?: ReadonlyArray<Auth>;
url: Url;
}
export interface ResolvedRequestOptions<
TResponseStyle extends ResponseStyle = "fields",
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
serializedBody?: string
serializedBody?: string;
}
export type RequestResult<
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
TResponseStyle extends ResponseStyle = 'fields',
> = ThrowOnError extends true
? Promise<
TResponseStyle extends "data"
TResponseStyle extends 'data'
? TData extends Record<string, unknown>
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData
request: Request
response: Response
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends "data"
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
TResponseStyle extends 'data'
?
| (TData extends Record<string, unknown>
? TData[keyof TData]
: TData)
| undefined
: (
| {
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData
error: undefined
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
error: undefined;
}
| {
data: undefined
error: TError extends Record<string, unknown> ? TError[keyof TError] : TError
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
}
) & {
request: Request
response: Response
request: Request;
response: Response;
}
>
>;
export interface ClientOptions {
baseUrl?: string
responseStyle?: ResponseStyle
throwOnError?: boolean
baseUrl?: string;
responseStyle?: ResponseStyle;
throwOnError?: boolean;
}
type MethodFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type SseFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
) => Promise<ServerSentEventsResult<TData, TError>>
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => Promise<ServerSentEventsResult<TData, TError>>;
type RequestFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> &
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
Pick<
Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
'method'
>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = <
TData extends {
body?: unknown
path?: Record<string, unknown>
query?: Record<string, unknown>
url: string
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
url: string;
},
>(
options: TData & Options<TData>,
) => string
) => string;
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>
}
export type Client = CoreClient<
RequestFn,
Config,
MethodFn,
BuildUrlFn,
SseFn
> & {
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
};
/**
* The `createClientConfig()` function will be called on client initialization
@@ -181,22 +217,25 @@ export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn>
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>
) => Config<Required<ClientOptions> & T>;
export interface TDataShape {
body?: unknown
headers?: unknown
path?: unknown
query?: unknown
url: string
body?: unknown;
headers?: unknown;
path?: unknown;
query?: unknown;
url: string;
}
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
TResponse = unknown,
TResponseStyle extends ResponseStyle = "fields",
> = OmitKeys<RequestOptions<TResponse, TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> &
([TData] extends [never] ? unknown : Omit<TData, "url">)
TResponseStyle extends ResponseStyle = 'fields',
> = OmitKeys<
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
'body' | 'path' | 'query' | 'url'
> &
([TData] extends [never] ? unknown : Omit<TData, 'url'>);
+161 -118
View File
@@ -1,289 +1,332 @@
// This file is auto-generated by @hey-api/openapi-ts
import { getAuthToken } from "../core/auth.gen.js"
import type { QuerySerializerOptions } from "../core/bodySerializer.gen.js"
import { jsonBodySerializer } from "../core/bodySerializer.gen.js"
import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen.js"
import { getUrl } from "../core/utils.gen.js"
import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen.js"
import { getAuthToken } from '../core/auth.gen.js';
import type { QuerySerializerOptions } from '../core/bodySerializer.gen.js';
import { jsonBodySerializer } from '../core/bodySerializer.gen.js';
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer.gen.js';
import { getUrl } from '../core/utils.gen.js';
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen.js';
export const createQuerySerializer = <T = unknown>({ parameters = {}, ...args }: QuerySerializerOptions = {}) => {
export const createQuerySerializer = <T = unknown>({
parameters = {},
...args
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = []
if (queryParams && typeof queryParams === "object") {
const search: string[] = [];
if (queryParams && typeof queryParams === 'object') {
for (const name in queryParams) {
const value = queryParams[name]
const value = queryParams[name];
if (value === undefined || value === null) {
continue
continue;
}
const options = parameters[name] || args
const options = parameters[name] || args;
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved: options.allowReserved,
explode: true,
name,
style: "form",
style: 'form',
value,
...options.array,
})
if (serializedArray) search.push(serializedArray)
} else if (typeof value === "object") {
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === 'object') {
const serializedObject = serializeObjectParam({
allowReserved: options.allowReserved,
explode: true,
name,
style: "deepObject",
style: 'deepObject',
value: value as Record<string, unknown>,
...options.object,
})
if (serializedObject) search.push(serializedObject)
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved: options.allowReserved,
name,
value: value as string,
})
if (serializedPrimitive) search.push(serializedPrimitive)
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join("&")
}
return querySerializer
}
return search.join('&');
};
return querySerializer;
};
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"], "auto"> => {
export const getParseAs = (
contentType: string | null,
): Exclude<Config['parseAs'], 'auto'> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
return "stream"
return 'stream';
}
const cleanContent = contentType.split(";")[0]?.trim()
const cleanContent = contentType.split(';')[0]?.trim();
if (!cleanContent) {
return
return;
}
if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) {
return "json"
if (
cleanContent.startsWith('application/json') ||
cleanContent.endsWith('+json')
) {
return 'json';
}
if (cleanContent === "multipart/form-data") {
return "formData"
if (cleanContent === 'multipart/form-data') {
return 'formData';
}
if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) {
return "blob"
if (
['application/', 'audio/', 'image/', 'video/'].some((type) =>
cleanContent.startsWith(type),
)
) {
return 'blob';
}
if (cleanContent.startsWith("text/")) {
return "text"
if (cleanContent.startsWith('text/')) {
return 'text';
}
return
}
return;
};
const checkForExistence = (
options: Pick<RequestOptions, "auth" | "query"> & {
headers: Headers
options: Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
},
name?: string,
): boolean => {
if (!name) {
return false
return false;
}
if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) {
return true
if (
options.headers.has(name) ||
options.query?.[name] ||
options.headers.get('Cookie')?.includes(`${name}=`)
) {
return true;
}
return false
}
return false;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, "security"> &
Pick<RequestOptions, "auth" | "query"> & {
headers: Headers
}: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
}) => {
for (const auth of security) {
if (checkForExistence(options, auth.name)) {
continue
continue;
}
const token = await getAuthToken(auth, options.auth)
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue
continue;
}
const name = auth.name ?? "Authorization"
const name = auth.name ?? 'Authorization';
switch (auth.in) {
case "query":
case 'query':
if (!options.query) {
options.query = {}
options.query = {};
}
options.query[name] = token
break
case "cookie":
options.headers.append("Cookie", `${name}=${token}`)
break
case "header":
options.query[name] = token;
break;
case 'cookie':
options.headers.append('Cookie', `${name}=${token}`);
break;
case 'header':
default:
options.headers.set(name, token)
break
options.headers.set(name, token);
break;
}
}
}
};
export const buildUrl: Client["buildUrl"] = (options) =>
export const buildUrl: Client['buildUrl'] = (options) =>
getUrl({
baseUrl: options.baseUrl as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === "function"
typeof options.querySerializer === 'function'
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
})
});
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b }
if (config.baseUrl?.endsWith("/")) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1)
const config = { ...a, ...b };
if (config.baseUrl?.endsWith('/')) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers)
return config
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
const headersEntries = (headers: Headers): Array<[string, string]> => {
const entries: Array<[string, string]> = []
const entries: Array<[string, string]> = [];
headers.forEach((value, key) => {
entries.push([key, value])
})
return entries
}
entries.push([key, value]);
});
return entries;
};
export const mergeHeaders = (...headers: Array<Required<Config>["headers"] | undefined>): Headers => {
const mergedHeaders = new Headers()
export const mergeHeaders = (
...headers: Array<Required<Config>['headers'] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header) {
continue
continue;
}
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header)
const iterator =
header instanceof Headers
? headersEntries(header)
: Object.entries(header);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key)
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, v as string)
mergedHeaders.append(key, v as string);
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string))
mergedHeaders.set(
key,
typeof value === 'object' ? JSON.stringify(value) : (value as string),
);
}
}
}
return mergedHeaders
}
return mergedHeaders;
};
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
options: Options,
) => Err | Promise<Err>
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>
type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (response: Res, request: Req, options: Options) => Res | Promise<Res>
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
class Interceptors<Interceptor> {
fns: Array<Interceptor | null> = []
fns: Array<Interceptor | null> = [];
clear(): void {
this.fns = []
this.fns = [];
}
eject(id: number | Interceptor): void {
const index = this.getInterceptorIndex(id)
const index = this.getInterceptorIndex(id);
if (this.fns[index]) {
this.fns[index] = null
this.fns[index] = null;
}
}
exists(id: number | Interceptor): boolean {
const index = this.getInterceptorIndex(id)
return Boolean(this.fns[index])
const index = this.getInterceptorIndex(id);
return Boolean(this.fns[index]);
}
getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === "number") {
return this.fns[id] ? id : -1
if (typeof id === 'number') {
return this.fns[id] ? id : -1;
}
return this.fns.indexOf(id)
return this.fns.indexOf(id);
}
update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false {
const index = this.getInterceptorIndex(id)
update(
id: number | Interceptor,
fn: Interceptor,
): number | Interceptor | false {
const index = this.getInterceptorIndex(id);
if (this.fns[index]) {
this.fns[index] = fn
return id
this.fns[index] = fn;
return id;
}
return false
return false;
}
use(fn: Interceptor): number {
this.fns.push(fn)
return this.fns.length - 1
this.fns.push(fn);
return this.fns.length - 1;
}
}
export interface Middleware<Req, Res, Err, Options> {
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>
request: Interceptors<ReqInterceptor<Req, Options>>
response: Interceptors<ResInterceptor<Res, Req, Options>>
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
request: Interceptors<ReqInterceptor<Req, Options>>;
response: Interceptors<ResInterceptor<Res, Req, Options>>;
}
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<Req, Res, Err, Options> => ({
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
Req,
Res,
Err,
Options
> => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
})
});
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: "form",
style: 'form',
},
object: {
explode: true,
style: "deepObject",
style: 'deepObject',
},
})
});
const defaultHeaders = {
"Content-Type": "application/json",
}
'Content-Type': 'application/json',
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
parseAs: "auto",
parseAs: 'auto',
querySerializer: defaultQuerySerializer,
...override,
})
});
+14 -13
View File
@@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts
export type AuthToken = string | undefined
export type AuthToken = string | undefined;
export interface Auth {
/**
@@ -8,34 +8,35 @@ export interface Auth {
*
* @default 'header'
*/
in?: "header" | "query" | "cookie"
in?: 'header' | 'query' | 'cookie';
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string
scheme?: "basic" | "bearer"
type: "apiKey" | "http"
name?: string;
scheme?: 'basic' | 'bearer';
type: 'apiKey' | 'http';
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token = typeof callback === "function" ? await callback(auth) : callback
const token =
typeof callback === 'function' ? await callback(auth) : callback;
if (!token) {
return
return;
}
if (auth.scheme === "bearer") {
return `Bearer ${token}`
if (auth.scheme === 'bearer') {
return `Bearer ${token}`;
}
if (auth.scheme === "basic") {
return `Basic ${btoa(token)}`
if (auth.scheme === 'basic') {
return `Basic ${btoa(token)}`;
}
return token
}
return token;
};
@@ -1,82 +1,100 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen.js"
import type {
ArrayStyle,
ObjectStyle,
SerializerOptions,
} from './pathSerializer.gen.js';
export type QuerySerializer = (query: Record<string, unknown>) => string
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: any) => any
export type BodySerializer = (body: any) => any;
type QuerySerializerOptionsObject = {
allowReserved?: boolean
array?: Partial<SerializerOptions<ArrayStyle>>
object?: Partial<SerializerOptions<ObjectStyle>>
}
allowReserved?: boolean;
array?: Partial<SerializerOptions<ArrayStyle>>;
object?: Partial<SerializerOptions<ObjectStyle>>;
};
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
/**
* Per-parameter serialization overrides. When provided, these settings
* override the global array/object settings for specific parameter names.
*/
parameters?: Record<string, QuerySerializerOptionsObject>
}
parameters?: Record<string, QuerySerializerOptionsObject>;
};
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
if (typeof value === "string" || value instanceof Blob) {
data.append(key, value)
const serializeFormDataPair = (
data: FormData,
key: string,
value: unknown,
): void => {
if (typeof value === 'string' || value instanceof Blob) {
data.append(key, value);
} else if (value instanceof Date) {
data.append(key, value.toISOString())
data.append(key, value.toISOString());
} else {
data.append(key, JSON.stringify(value))
data.append(key, JSON.stringify(value));
}
}
};
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => {
if (typeof value === "string") {
data.append(key, value)
const serializeUrlSearchParamsPair = (
data: URLSearchParams,
key: string,
value: unknown,
): void => {
if (typeof value === 'string') {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value))
data.append(key, JSON.stringify(value));
}
}
};
export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): FormData => {
const data = new FormData()
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): FormData => {
const data = new FormData();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v))
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(data, key, value)
serializeFormDataPair(data, key, value);
}
})
});
return data
return data;
},
}
};
export const jsonBodySerializer = {
bodySerializer: <T>(body: T): string =>
JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)),
}
JSON.stringify(body, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value,
),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): string => {
const data = new URLSearchParams()
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): string => {
const data = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v))
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value)
serializeUrlSearchParamsPair(data, key, value);
}
})
});
return data.toString()
return data.toString();
},
}
};
+70 -63
View File
@@ -1,160 +1,167 @@
// This file is auto-generated by @hey-api/openapi-ts
type Slot = "body" | "headers" | "path" | "query"
type Slot = 'body' | 'headers' | 'path' | 'query';
export type Field =
| {
in: Exclude<Slot, "body">
in: Exclude<Slot, 'body'>;
/**
* Field name. This is the name we want the user to see and use.
*/
key: string
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If omitted, we use the same value as `key`.
*/
map?: string
map?: string;
}
| {
in: Extract<Slot, "body">
in: Extract<Slot, 'body'>;
/**
* Key isn't required for bodies.
*/
key?: string
map?: string
key?: string;
map?: string;
}
| {
/**
* Field name. This is the name we want the user to see and use.
*/
key: string
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If `in` is omitted, `map` aliases `key` to the transport layer.
*/
map: Slot
}
map: Slot;
};
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>
args?: ReadonlyArray<Field>
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
}
export type FieldsConfig = ReadonlyArray<Field | Fields>
export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = {
$body_: "body",
$headers_: "headers",
$path_: "path",
$query_: "query",
}
const extraPrefixes = Object.entries(extraPrefixesMap)
$body_: 'body',
$headers_: 'headers',
$path_: 'path',
$query_: 'query',
};
const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map<
string,
| {
in: Slot
map?: string
in: Slot;
map?: string;
}
| {
in?: never
map: Slot
in?: never;
map: Slot;
}
>
>;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map()
map = new Map();
}
for (const config of fields) {
if ("in" in config) {
if ('in' in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
})
});
}
} else if ("key" in config) {
} else if ('key' in config) {
map.set(config.key, {
map: config.map,
})
});
} else if (config.args) {
buildKeyMap(config.args, map)
buildKeyMap(config.args, map);
}
}
return map
}
return map;
};
interface Params {
body: unknown
headers: Record<string, unknown>
path: Record<string, unknown>
query: Record<string, unknown>
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === "object" && !Object.keys(value).length) {
delete params[slot as Slot]
if (value && typeof value === 'object' && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
}
};
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
export const buildClientParams = (
args: ReadonlyArray<unknown>,
fields: FieldsConfig,
) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
}
};
const map = buildKeyMap(fields)
const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined
let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index]
config = fields[index];
}
if (!config) {
continue
continue;
}
if ("in" in config) {
if ('in' in config) {
if (config.key) {
const field = map.get(config.key)!
const name = field.map || config.key
const field = map.get(config.key)!;
const name = field.map || config.key;
if (field.in) {
;(params[field.in] as Record<string, unknown>)[name] = arg
(params[field.in] as Record<string, unknown>)[name] = arg;
}
} else {
params.body = arg
params.body = arg;
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key)
const field = map.get(key);
if (field) {
if (field.in) {
const name = field.map || key
;(params[field.in] as Record<string, unknown>)[name] = value
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
} else {
params[field.map] = value
params[field.map] = value;
}
} else {
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix))
const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix),
);
if (extra) {
const [prefix, slot] = extra
;(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value
} else if ("allowExtra" in config && config.allowExtra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[
key.slice(prefix.length)
] = value;
} else if ('allowExtra' in config && config.allowExtra) {
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
if (allowed) {
;(params[slot as Slot] as Record<string, unknown>)[key] = value
break
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
}
}
}
@@ -163,7 +170,7 @@ export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsCo
}
}
stripEmptySlots(params)
stripEmptySlots(params);
return params
}
return params;
};
@@ -1,68 +1,70 @@
// This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean
name: string
allowReserved?: boolean;
name: string;
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean
style: T
explode: boolean;
style: T;
}
export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle
type MatrixStyle = "label" | "matrix" | "simple"
export type ObjectStyle = "form" | "deepObject"
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = 'label' | 'matrix' | 'simple';
export type ObjectStyle = 'form' | 'deepObject';
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string
value: string;
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case "label":
return "."
case "matrix":
return ";"
case "simple":
return ","
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return "&"
return '&';
}
}
};
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case "form":
return ","
case "pipeDelimited":
return "|"
case "spaceDelimited":
return "%20"
case 'form':
return ',';
case 'pipeDelimited':
return '|';
case 'spaceDelimited':
return '%20';
default:
return ","
return ',';
}
}
};
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case "label":
return "."
case "matrix":
return ";"
case "simple":
return ","
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return "&"
return '&';
}
}
};
export const serializeArrayParam = ({
allowReserved,
@@ -71,54 +73,60 @@ export const serializeArrayParam = ({
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[]
value: unknown[];
}) => {
if (!explode) {
const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join(
separatorArrayNoExplode(style),
)
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
switch (style) {
case "label":
return `.${joinedValues}`
case "matrix":
return `;${name}=${joinedValues}`
case "simple":
return joinedValues
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
case 'simple':
return joinedValues;
default:
return `${name}=${joinedValues}`
return `${name}=${joinedValues}`;
}
}
const separator = separatorArrayExplode(style)
const separator = separatorArrayExplode(style);
const joinedValues = value
.map((v) => {
if (style === "label" || style === "simple") {
return allowReserved ? v : encodeURIComponent(v as string)
if (style === 'label' || style === 'simple') {
return allowReserved ? v : encodeURIComponent(v as string);
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
})
});
})
.join(separator)
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues
}
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
};
export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => {
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return ""
return '';
}
if (typeof value === "object") {
if (typeof value === 'object') {
throw new Error(
"Deeply-nested arrays/objects arent supported. Provide your own `querySerializer()` to handle these.",
)
'Deeply-nested arrays/objects arent supported. Provide your own `querySerializer()` to handle these.',
);
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
export const serializeObjectParam = ({
allowReserved,
@@ -128,40 +136,46 @@ export const serializeObjectParam = ({
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date
valueOnly?: boolean
value: Record<string, unknown> | Date;
valueOnly?: boolean;
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
}
if (style !== "deepObject" && !explode) {
let values: string[] = []
if (style !== 'deepObject' && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]
})
const joinedValues = values.join(",")
values = [
...values,
key,
allowReserved ? (v as string) : encodeURIComponent(v as string),
];
});
const joinedValues = values.join(',');
switch (style) {
case "form":
return `${name}=${joinedValues}`
case "label":
return `.${joinedValues}`
case "matrix":
return `;${name}=${joinedValues}`
case 'form':
return `${name}=${joinedValues}`;
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
default:
return joinedValues
return joinedValues;
}
}
const separator = separatorObjectExplode(style)
const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === "deepObject" ? `${name}[${key}]` : key,
name: style === 'deepObject' ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator)
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues
}
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
};
@@ -3,109 +3,134 @@
/**
* JSON-friendly union that mirrors what Pinia Colada can hash.
*/
export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue }
export type JsonValue =
| null
| string
| number
| boolean
| JsonValue[]
| { [key: string]: JsonValue };
/**
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
*/
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
if (value === undefined || typeof value === "function" || typeof value === "symbol") {
return undefined
if (
value === undefined ||
typeof value === 'function' ||
typeof value === 'symbol'
) {
return undefined;
}
if (typeof value === "bigint") {
return value.toString()
if (typeof value === 'bigint') {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString()
return value.toISOString();
}
return value
}
return value;
};
/**
* Safely stringifies a value and parses it back into a JsonValue.
*/
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
try {
const json = JSON.stringify(input, queryKeyJsonReplacer)
const json = JSON.stringify(input, queryKeyJsonReplacer);
if (json === undefined) {
return undefined
return undefined;
}
return JSON.parse(json) as JsonValue
return JSON.parse(json) as JsonValue;
} catch {
return undefined
return undefined;
}
}
};
/**
* Detects plain objects (including objects with a null prototype).
*/
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
if (value === null || typeof value !== "object") {
return false
if (value === null || typeof value !== 'object') {
return false;
}
const prototype = Object.getPrototypeOf(value as object)
return prototype === Object.prototype || prototype === null
}
const prototype = Object.getPrototypeOf(value as object);
return prototype === Object.prototype || prototype === null;
};
/**
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
*/
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b))
const result: Record<string, JsonValue> = {}
const entries = Array.from(params.entries()).sort(([a], [b]) =>
a.localeCompare(b),
);
const result: Record<string, JsonValue> = {};
for (const [key, value] of entries) {
const existing = result[key]
const existing = result[key];
if (existing === undefined) {
result[key] = value
continue
result[key] = value;
continue;
}
if (Array.isArray(existing)) {
;(existing as string[]).push(value)
(existing as string[]).push(value);
} else {
result[key] = [existing, value]
result[key] = [existing, value];
}
}
return result
}
return result;
};
/**
* Normalizes any accepted value into a JSON-friendly shape for query keys.
*/
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
export const serializeQueryKeyValue = (
value: unknown,
): JsonValue | undefined => {
if (value === null) {
return null
return null;
}
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return value
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
return value;
}
if (value === undefined || typeof value === "function" || typeof value === "symbol") {
return undefined
if (
value === undefined ||
typeof value === 'function' ||
typeof value === 'symbol'
) {
return undefined;
}
if (typeof value === "bigint") {
return value.toString()
if (typeof value === 'bigint') {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString()
return value.toISOString();
}
if (Array.isArray(value)) {
return stringifyToJsonValue(value)
return stringifyToJsonValue(value);
}
if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) {
return serializeSearchParams(value)
if (
typeof URLSearchParams !== 'undefined' &&
value instanceof URLSearchParams
) {
return serializeSearchParams(value);
}
if (isPlainObject(value)) {
return stringifyToJsonValue(value)
return stringifyToJsonValue(value);
}
return undefined
}
return undefined;
};
@@ -1,20 +1,23 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Config } from "./types.gen.js"
import type { Config } from './types.gen.js';
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method"> &
Pick<Config, "method" | "responseTransformer" | "responseValidator"> & {
export type ServerSentEventsOptions<TData = unknown> = Omit<
RequestInit,
'method'
> &
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: typeof fetch
fetch?: typeof fetch;
/**
* Implementing clients can call request interceptors inside this hook.
*/
onRequest?: (url: string, init: RequestInit) => Promise<Request>
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
/**
* Callback invoked when a network or parsing error occurs during streaming.
*
@@ -22,7 +25,7 @@ export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method
*
* @param error The error that occurred.
*/
onSseError?: (error: unknown) => void
onSseError?: (error: unknown) => void;
/**
* Callback invoked when an event is streamed from the server.
*
@@ -31,8 +34,8 @@ export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method
* @param event Event streamed from the server.
* @returns Nothing (void).
*/
onSseEvent?: (event: StreamEvent<TData>) => void
serializedBody?: RequestInit["body"]
onSseEvent?: (event: StreamEvent<TData>) => void;
serializedBody?: RequestInit['body'];
/**
* Default retry delay in milliseconds.
*
@@ -40,11 +43,11 @@ export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method
*
* @default 3000
*/
sseDefaultRetryDelay?: number
sseDefaultRetryDelay?: number;
/**
* Maximum number of retry attempts before giving up.
*/
sseMaxRetryAttempts?: number
sseMaxRetryAttempts?: number;
/**
* Maximum retry delay in milliseconds.
*
@@ -54,26 +57,34 @@ export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method
*
* @default 30000
*/
sseMaxRetryDelay?: number
sseMaxRetryDelay?: number;
/**
* Optional sleep function for retry backoff.
*
* Defaults to using `setTimeout`.
*/
sseSleepFn?: (ms: number) => Promise<void>
url: string
}
sseSleepFn?: (ms: number) => Promise<void>;
url: string;
};
export interface StreamEvent<TData = unknown> {
data: TData
event?: string
id?: string
retry?: number
data: TData;
event?: string;
id?: string;
retry?: number;
}
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
stream: AsyncGenerator<TData extends Record<string, unknown> ? TData[keyof TData] : TData, TReturn, TNext>
}
export type ServerSentEventsResult<
TData = unknown,
TReturn = void,
TNext = unknown,
> = {
stream: AsyncGenerator<
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
TReturn,
TNext
>;
};
export const createSseClient = <TData = unknown>({
onRequest,
@@ -88,113 +99,123 @@ export const createSseClient = <TData = unknown>({
url,
...options
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
let lastEventId: string | undefined
let lastEventId: string | undefined;
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)))
const sleep =
sseSleepFn ??
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
const createStream = async function* () {
let retryDelay: number = sseDefaultRetryDelay ?? 3000
let attempt = 0
const signal = options.signal ?? new AbortController().signal
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
let attempt = 0;
const signal = options.signal ?? new AbortController().signal;
while (true) {
if (signal.aborted) break
if (signal.aborted) break;
attempt++
attempt++;
const headers =
options.headers instanceof Headers
? options.headers
: new Headers(options.headers as Record<string, string> | undefined)
: new Headers(options.headers as Record<string, string> | undefined);
if (lastEventId !== undefined) {
headers.set("Last-Event-ID", lastEventId)
headers.set('Last-Event-ID', lastEventId);
}
try {
const requestInit: RequestInit = {
redirect: "follow",
redirect: 'follow',
...options,
body: options.serializedBody,
headers,
signal,
}
let request = new Request(url, requestInit)
};
let request = new Request(url, requestInit);
if (onRequest) {
request = await onRequest(url, requestInit)
request = await onRequest(url, requestInit);
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = options.fetch ?? globalThis.fetch
const response = await _fetch(request)
const _fetch = options.fetch ?? globalThis.fetch;
const response = await _fetch(request);
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`)
if (!response.ok)
throw new Error(
`SSE failed: ${response.status} ${response.statusText}`,
);
if (!response.body) throw new Error("No body in SSE response")
if (!response.body) throw new Error('No body in SSE response');
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
let buffer = ""
let buffer = '';
const abortHandler = () => {
try {
reader.cancel()
reader.cancel();
} catch {
// noop
}
}
};
signal.addEventListener("abort", abortHandler)
signal.addEventListener('abort', abortHandler);
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += value
const { done, value } = await reader.read();
if (done) break;
buffer += value;
const chunks = buffer.split("\n\n")
buffer = chunks.pop() ?? ""
const chunks = buffer.split('\n\n');
buffer = chunks.pop() ?? '';
for (const chunk of chunks) {
const lines = chunk.split("\n")
const dataLines: Array<string> = []
let eventName: string | undefined
const lines = chunk.split('\n');
const dataLines: Array<string> = [];
let eventName: string | undefined;
for (const line of lines) {
if (line.startsWith("data:")) {
dataLines.push(line.replace(/^data:\s*/, ""))
} else if (line.startsWith("event:")) {
eventName = line.replace(/^event:\s*/, "")
} else if (line.startsWith("id:")) {
lastEventId = line.replace(/^id:\s*/, "")
} else if (line.startsWith("retry:")) {
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10)
if (line.startsWith('data:')) {
dataLines.push(line.replace(/^data:\s*/, ''));
} else if (line.startsWith('event:')) {
eventName = line.replace(/^event:\s*/, '');
} else if (line.startsWith('id:')) {
lastEventId = line.replace(/^id:\s*/, '');
} else if (line.startsWith('retry:')) {
const parsed = Number.parseInt(
line.replace(/^retry:\s*/, ''),
10,
);
if (!Number.isNaN(parsed)) {
retryDelay = parsed
retryDelay = parsed;
}
}
}
let data: unknown
let parsedJson = false
let data: unknown;
let parsedJson = false;
if (dataLines.length) {
const rawData = dataLines.join("\n")
const rawData = dataLines.join('\n');
try {
data = JSON.parse(rawData)
parsedJson = true
data = JSON.parse(rawData);
parsedJson = true;
} catch {
data = rawData
data = rawData;
}
}
if (parsedJson) {
if (responseValidator) {
await responseValidator(data)
await responseValidator(data);
}
if (responseTransformer) {
data = await responseTransformer(data)
data = await responseTransformer(data);
}
}
@@ -203,35 +224,41 @@ export const createSseClient = <TData = unknown>({
event: eventName,
id: lastEventId,
retry: retryDelay,
})
});
if (dataLines.length) {
yield data as any
yield data as any;
}
}
}
} finally {
signal.removeEventListener("abort", abortHandler)
reader.releaseLock()
signal.removeEventListener('abort', abortHandler);
reader.releaseLock();
}
break // exit loop on normal completion
break; // exit loop on normal completion
} catch (error) {
// connection failed or aborted; retry after delay
onSseError?.(error)
onSseError?.(error);
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
break // stop after firing error
if (
sseMaxRetryAttempts !== undefined &&
attempt >= sseMaxRetryAttempts
) {
break; // stop after firing error
}
// exponential backoff: double retry each attempt, cap at 30s
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000)
await sleep(backoff)
const backoff = Math.min(
retryDelay * 2 ** (attempt - 1),
sseMaxRetryDelay ?? 30000,
);
await sleep(backoff);
}
}
}
};
const stream = createStream()
const stream = createStream();
return { stream }
}
return { stream };
};
+54 -22
View File
@@ -1,33 +1,54 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth, AuthToken } from "./auth.gen.js"
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen.js"
import type { Auth, AuthToken } from './auth.gen.js';
import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from './bodySerializer.gen.js';
export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"
export type HttpMethod =
| 'connect'
| 'delete'
| 'get'
| 'head'
| 'options'
| 'patch'
| 'post'
| 'put'
| 'trace';
export type Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, SseFn = never> = {
export type Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
SseFn = never,
> = {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn
getConfig: () => Config
request: RequestFn
setConfig: (config: Config) => Config
buildUrl: BuildUrlFn;
getConfig: () => Config;
request: RequestFn;
setConfig: (config: Config) => Config;
} & {
[K in HttpMethod]: MethodFn
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } })
[K in HttpMethod]: MethodFn;
} & ([SseFn] extends [never]
? { sse?: never }
: { sse: { [K in HttpMethod]: SseFn } });
export interface Config {
/**
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null
bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
@@ -35,14 +56,23 @@ export interface Config {
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit["headers"]
| Record<string, string | number | boolean | (string | number | boolean)[] | null | undefined | unknown>
| RequestInit['headers']
| Record<
string,
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined
| unknown
>;
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?: Uppercase<HttpMethod>
method?: Uppercase<HttpMethod>;
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
@@ -53,24 +83,24 @@ export interface Config {
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function validating request data. This is useful if you want to ensure
* the request conforms to the desired shape, so it can be safely sent to
* the server.
*/
requestValidator?: (data: unknown) => Promise<unknown>
requestValidator?: (data: unknown) => Promise<unknown>;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>
responseValidator?: (data: unknown) => Promise<unknown>;
}
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
@@ -79,8 +109,10 @@ type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
? [undefined] extends [T]
? false
: true
: false
: false;
export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K]
}
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
? never
: K]: T[K];
};
+64 -58
View File
@@ -1,54 +1,57 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen.js"
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen.js';
import {
type ArraySeparatorStyle,
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from "./pathSerializer.gen.js"
} from './pathSerializer.gen.js';
export interface PathSerializer {
path: Record<string, unknown>
url: string
path: Record<string, unknown>;
url: string;
}
export const PATH_PARAM_RE = /\{[^{}]+\}/g
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url
const matches = _url.match(PATH_PARAM_RE)
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false
let name = match.substring(1, match.length - 1)
let style: ArraySeparatorStyle = "simple"
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = 'simple';
if (name.endsWith("*")) {
explode = true
name = name.substring(0, name.length - 1)
if (name.endsWith('*')) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith(".")) {
name = name.substring(1)
style = "label"
} else if (name.startsWith(";")) {
name = name.substring(1)
style = "matrix"
if (name.startsWith('.')) {
name = name.substring(1);
style = 'label';
} else if (name.startsWith(';')) {
name = name.substring(1);
style = 'matrix';
}
const value = path[name]
const value = path[name];
if (value === undefined || value === null) {
continue
continue;
}
if (Array.isArray(value)) {
url = url.replace(match, serializeArrayParam({ explode, name, style, value }))
continue
url = url.replace(
match,
serializeArrayParam({ explode, name, style, value }),
);
continue;
}
if (typeof value === "object") {
if (typeof value === 'object') {
url = url.replace(
match,
serializeObjectParam({
@@ -58,27 +61,29 @@ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
value: value as Record<string, unknown>,
valueOnly: true,
}),
)
continue
);
continue;
}
if (style === "matrix") {
if (style === 'matrix') {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
)
continue
);
continue;
}
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string))
url = url.replace(match, replaceValue)
const replaceValue = encodeURIComponent(
style === 'label' ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url
}
return url;
};
export const getUrl = ({
baseUrl,
@@ -87,51 +92,52 @@ export const getUrl = ({
querySerializer,
url: _url,
}: {
baseUrl?: string
path?: Record<string, unknown>
query?: Record<string, unknown>
querySerializer: QuerySerializer
url: string
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`
let url = (baseUrl ?? "") + pathUrl
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? '') + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url })
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : ""
if (search.startsWith("?")) {
search = search.substring(1)
let search = query ? querySerializer(query) : '';
if (search.startsWith('?')) {
search = search.substring(1);
}
if (search) {
url += `?${search}`
url += `?${search}`;
}
return url
}
return url;
};
export function getValidRequestBody(options: {
body?: unknown
bodySerializer?: BodySerializer | null
serializedBody?: unknown
body?: unknown;
bodySerializer?: BodySerializer | null;
serializedBody?: unknown;
}) {
const hasBody = options.body !== undefined
const isSerializedBody = hasBody && options.bodySerializer
const hasBody = options.body !== undefined;
const isSerializedBody = hasBody && options.bodySerializer;
if (isSerializedBody) {
if ("serializedBody" in options) {
const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== ""
if ('serializedBody' in options) {
const hasSerializedBody =
options.serializedBody !== undefined && options.serializedBody !== '';
return hasSerializedBody ? options.serializedBody : null
return hasSerializedBody ? options.serializedBody : null;
}
// not all clients implement a serializedBody property (i.e. client-axios)
return options.body !== "" ? options.body : null
return options.body !== '' ? options.body : null;
}
// plain/text body
if (hasBody) {
return options.body
return options.body;
}
// no body was provided
return undefined
return undefined;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2074 -281
View File
File diff suppressed because it is too large Load Diff