rest: support headers, FormData and providing own fetch client
@@ -13,28 +13,14 @@ "peerDependencies": {
"typescript": "^5", }, }, - "pkgs/examples": { - "name": "@purrkit/examples", - "dependencies": { - "@purrkit/rest": "workspace:*", - "@purrkit/router": "workspace:*", - "@purrkit/types": "workspace:*", - }, - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, "pkgs/rest": { "name": "@purrkit/rest", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@purrkit/types": "workspace:*", }, "devDependencies": { - "@types/bun": "latest", + "@types/node": "latest", }, "peerDependencies": { "typescript": "^5",@@ -53,7 +39,7 @@ },
}, "pkgs/types": { "name": "@purrkit/types", - "version": "1.1.0", + "version": "1.1.1", "devDependencies": { "@types/bun": "latest", },@@ -151,8 +137,6 @@ "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.68.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-NnKXr4Wgo4nps3erhrE0f8shBvBPZMHg72nDsvX0JyrRvsNiP3f1JNvbCKh+A6VFvpF7ZoJxu904P3cKMhvZnA=="],
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.68.0", "", { "os": "win32", "cpu": "x64" }, "sha512-zg5pA+84AlU6XHJ3ruiRxziO71QTrz8nLsk6u01JGS5+tL9/bnlakFiklFrcy4R1/V7ktWtaNitN3JZWmKnf6g=="], - "@purrkit/examples": ["@purrkit/examples@workspace:pkgs/examples"], - "@purrkit/rest": ["@purrkit/rest@workspace:pkgs/rest"], "@purrkit/router": ["@purrkit/router@workspace:pkgs/router"],@@ -167,7 +151,7 @@ "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],@@ -201,7 +185,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], - "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],@@ -210,5 +194,13 @@
"@discordjs/rest/@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@types/ws/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + + "bun-types/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + + "@types/ws/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "bun-types/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], } }
@@ -1,6 +1,6 @@
{ "name": "@purrkit/rest", - "version": "1.0.1", + "version": "1.1.0", "license": "EUPL-1.2", "files": [ "dist",@@ -23,7 +23,7 @@ "dependencies": {
"@purrkit/types": "workspace:*" }, "devDependencies": { - "@types/bun": "latest" + "@types/node": "latest" }, "peerDependencies": { "typescript": "^5"
@@ -3,16 +3,20 @@
export interface ClientOptions { token: string; baseUrl?: string; + fetch?: typeof fetch; } export function RESTClient(options: ClientOptions): DiscordRestClient { const baseUrl = options.baseUrl || "https://discord.com/api/v10"; + + const customFetch = options.fetch ?? globalThis.fetch; const executeRequest = async ( segments: string[], method: string, body?: unknown, query?: Record<string, unknown>, + headers?: Record<string, string>, ): Promise<ApiResponse<unknown>> => { let url = `${baseUrl}/${segments.join("/")}`;@@ -20,7 +24,13 @@ if (query) {
const searchParams = new URLSearchParams(); for (const [key, val] of Object.entries(query)) { if (val !== undefined && val !== null) { - searchParams.append(key, String(val)); + if (Array.isArray(val)) { + for (const item of val) { + searchParams.append(key, String(item)); + } + } else { + searchParams.append(key, String(val)); + } } } const queryString = searchParams.toString();@@ -28,13 +38,24 @@ if (queryString) url += `?${queryString}`;
} try { - const res = await fetch(url, { + const isFormData = typeof FormData !== "undefined" && body instanceof FormData; + + const reqHeaders: Record<string, string> = { + Authorization: `Bot ${options.token}`, + ...headers, + }; + + if (!isFormData && !reqHeaders["Content-Type"] && body !== undefined) + reqHeaders["Content-Type"] = "application/json"; + + const res = await customFetch(url, { method, - headers: { - Authorization: `Bot ${options.token}`, - "Content-Type": "application/json", - }, - body: body ? JSON.stringify(body) : undefined, + headers: reqHeaders, + body: isFormData + ? (body as BodyInit) + : body !== undefined + ? JSON.stringify(body) + : undefined, }); const isOk = res.ok;@@ -95,19 +116,25 @@ if (!httpMethod) throw new Error(`unsupported HTTP method: ${lowercaseProp}`);
return async ( bodyOrOptions?: unknown, - methodOptions?: { query?: Record<string, unknown> }, + methodOptions?: { query?: Record<string, unknown>; headers?: Record<string, string> }, ) => { let body: unknown; let query: Record<string, unknown> | undefined; + let headers: Record<string, string> | undefined; if (httpMethod === "GET" || httpMethod === "DELETE") { - query = (bodyOrOptions as { query?: Record<string, unknown> } | undefined)?.query; + const opts = bodyOrOptions as + | { query?: Record<string, unknown>; headers?: Record<string, string> } + | undefined; + query = opts?.query; + headers = opts?.headers; } else { body = bodyOrOptions; query = methodOptions?.query; + headers = methodOptions?.headers; } - return executeRequest(pathSegments, httpMethod, body, query); + return executeRequest(pathSegments, httpMethod, body, query, headers); }; }
@@ -82,10 +82,19 @@ ? "delete"
: M; type MethodFunc<Op, Method> = Method extends "get" - ? (options?: { query?: GetQuery<Op> }) => Promise<ResolveResponse<Op>> + ? (options?: { + query?: GetQuery<Op>; + headers?: Record<string, string>; + }) => Promise<ResolveResponse<Op>> : [GetBody<Op>] extends [never] - ? (body?: undefined, options?: { query?: GetQuery<Op> }) => Promise<ResolveResponse<Op>> - : (body: GetBody<Op>, options?: { query?: GetQuery<Op> }) => Promise<ResolveResponse<Op>>; + ? ( + body?: undefined, + options?: { query?: GetQuery<Op>; headers?: Record<string, string> }, + ) => Promise<ResolveResponse<Op>> + : ( + body: GetBody<Op> | FormData, + options?: { query?: GetQuery<Op>; headers?: Record<string, string> }, + ) => Promise<ResolveResponse<Op>>; type MapOperations<Operations> = { [Method in keyof Operations as MapVerb<Method>]: MethodFunc<Operations[Method], Method>;
@@ -4,7 +4,7 @@ "outDir": "dist",
"declaration": true, // Environment setup & latest features - "lib": ["ESNext"], + "lib": ["DOM", "ESNext"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force",