-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from FxOmar/feature/no-create
Use Nixify without creating a new instance
- Loading branch information
Showing
12 changed files
with
309 additions
and
273 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { ServiceConfig, XOR, ServiceReqMethods, RequestMethods } from '../types/index.js'; | ||
|
||
declare const _default: { | ||
create: <T extends ServiceConfig>(config?: T) => XOR<ServiceReqMethods<T>, RequestMethods>; | ||
}; | ||
|
||
export { _default as default }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export { a as default } from '../chunk-K5WNYHMN.js'; | ||
import '../chunk-QKR4Q4CV.js'; | ||
import '../chunk-J2XKEFHB.js'; | ||
import '../chunk-57VF4OXY.js'; | ||
import '../chunk-BOYYI7C6.js'; | ||
import '../chunk-45JH5YAD.js'; | ||
import '../chunk-BE5ABZLV.js'; | ||
import '../chunk-XRZHORBG.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import { ServiceConfig, XOR, ServiceReqMethods, RequestMethods } from './types/index.js'; | ||
import { RequestMethods, ServiceConfig, XOR, ServiceReqMethods } from './types/index.js'; | ||
|
||
declare const _default: { | ||
declare const _default: (RequestMethods & Record<string, never>) | ({ | ||
create: <T extends ServiceConfig>(config?: T) => XOR<ServiceReqMethods<T>, RequestMethods>; | ||
}; | ||
} & Record<string, never>); | ||
|
||
export { _default as default }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,12 @@ | ||
import { a as a$1 } from './chunk-QKR4Q4CV.js'; | ||
import { a } from './chunk-K5WNYHMN.js'; | ||
import './chunk-QKR4Q4CV.js'; | ||
import './chunk-J2XKEFHB.js'; | ||
import './chunk-57VF4OXY.js'; | ||
import { e, a, g as g$1, f } from './chunk-ULYLNEH2.js'; | ||
import { a as a$2 } from './chunk-BE5ABZLV.js'; | ||
import { a as a$3 } from './chunk-XRZHORBG.js'; | ||
import './chunk-BOYYI7C6.js'; | ||
import './chunk-45JH5YAD.js'; | ||
import './chunk-BE5ABZLV.js'; | ||
import './chunk-XRZHORBG.js'; | ||
|
||
var T=["get","head","put","delete","post","patch","options"],b={json:"application/json",text:"text/*",formData:"multipart/form-data",arrayBuffer:"*/*",blob:"*/*"},m=t=>{if(t.url.protocol!=="https:"&&t.url.protocol!=="http:")throw new TypeError(`Unsupported protocol, ${t.url.protocol}`);let o=f(t);return new Request(t.url.toString(),o)},q=async(t,o,r)=>{let n=g$1(t,r,o),s=m(n),e=await a$1(s,n);if(!e.ok)throw new a$2(e.clone(),s);let p={data:null,headers:e.headers,status:e.status,statusText:e.statusText,config:s},a=e.clone(),c=async()=>{if(n.responseType==="json"){if(e.status===204)return "";let u=await a.text();return u.length===0?"":a$3.parse(u)}return await a[n.responseType]()};if(n.responseType)try{p.data=await c();}catch{throw new TypeError(`Unsupported response type "${n.responseType}" specified in the request. The Content-Type of the response is "${e.headers.get("Content-Type")}".`)}return p},w=t=>{let o={};return T.forEach(r=>{o[r]=(n,s)=>{let e$1=s?.responseType||"json",p={...Object.fromEntries(Object.entries(b).map(([a,c])=>[a,()=>(e(t.headers=t?.headers||{},{accept:c}),e$1=a,p)])),then(...a){return q(t,r,{path:n,responseType:e$1,...s}).then(...a)},catch(a){return p.then().catch(a)}};return p};}),o},O=t=>Object.fromEntries(["beforeRetry","afterResponse","beforeRequest"].map(r=>[r,n=>{if(t?.hooks?.[r])throw new TypeError(`${r} has already been invoked within configuration.`);Object.assign(t,{hooks:{...t.hooks||{},[r]:n}});}])),j=t=>{let o=Object.fromEntries(Object.entries(t||{default:{}}).map(([s,e$1])=>[s,{...w(e$1),...O(e$1),setHeaders:p=>e(e$1.headers=e$1.headers||{},p)}])),r=s=>Object.values(o).forEach(s);return a(t)?{...o.default}:{...o,...o[Object.keys(o)[0]],beforeRequest:s=>r(e=>e.beforeRequest(s)),afterResponse:s=>r(e=>e.afterResponse(s)),beforeRetry:s=>r(e=>e.beforeRetry(s)),setHeaders:s=>r(e=>e.setHeaders(s))}},g={create:j}; | ||
var o={create:a.create,...a.create()}; | ||
|
||
export { g as default }; | ||
export { o as default }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
export { f as filterRequestOptions, a as isEmpty, b as isPositiveInteger, g as mergeConfigs, c as mergeHeaders, d as processHeaders, e as setHeaders } from '../chunk-ULYLNEH2.js'; | ||
import '../chunk-BE5ABZLV.js'; | ||
export { f as filterRequestOptions, a as isEmpty, b as isPositiveInteger, g as mergeConfigs, c as mergeHeaders, d as processHeaders, e as setHeaders } from '../chunk-BOYYI7C6.js'; | ||
import '../chunk-45JH5YAD.js'; | ||
import '../chunk-BE5ABZLV.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
import type { | ||
HttpMethod, | ||
MethodConfig, | ||
Options, | ||
RequestMethods, | ||
ResponseTypes, | ||
ResponseType, | ||
ServiceConfig, | ||
ServiceReqMethods, | ||
XOR, | ||
} from "../types" | ||
import { isEmpty, setHeaders, mergeConfigs, filterRequestOptions } from "../utils" | ||
import json from "../utils/json-parse" | ||
import { HTTPError } from "../utils/errors" | ||
import fetchRetry from "./fetchRetry" | ||
|
||
// All the HTTP request methods. | ||
const methods = ["get", "head", "put", "delete", "post", "patch", "options"] as const | ||
|
||
const responseTypes = { | ||
json: "application/json", | ||
text: "text/*", | ||
formData: "multipart/form-data", | ||
arrayBuffer: "*/*", | ||
blob: "*/*", | ||
} as ResponseTypes | ||
|
||
const _request = (config): Request => { | ||
if (config.url.protocol !== "https:" && config.url.protocol !== "http:") { | ||
throw new TypeError(`Unsupported protocol, ${config.url.protocol}`) | ||
} | ||
|
||
// if .json() is set by default add Accept header | ||
config.responseType === "json" && !config.headers.has("Accept") | ||
? config.headers.set("Accept", responseTypes.json) | ||
: undefined | ||
|
||
const request = filterRequestOptions(config) | ||
|
||
return new Request(config.url.toString(), request) | ||
} | ||
|
||
/** | ||
* HttpAdapter make http requests 🦅. | ||
* | ||
* @returns {Promise<ResponseInterface<U>>} | ||
*/ | ||
const httpAdapter = async (config: Options, method: HttpMethod, methodConfig: MethodConfig) => { | ||
const _config = mergeConfigs(config, methodConfig, method) | ||
const request = _request(_config) | ||
|
||
const response = await fetchRetry(request, _config) | ||
|
||
// non-2xx HTTP responses into errors: | ||
if (!response.ok) { | ||
throw new HTTPError(response.clone(), request) | ||
} | ||
|
||
// Response Schema | ||
const _response = { | ||
data: null, | ||
headers: response.headers, | ||
status: response.status, | ||
statusText: response.statusText, | ||
config: request, | ||
} | ||
|
||
const awaitedResponse = response.clone() | ||
|
||
const parseResponse = async () => { | ||
if (_config.responseType === "json") { | ||
// https://datatracker.ietf.org/doc/html/rfc2616#section-10.2.5 | ||
if (response.status === 204) return "" | ||
|
||
const data = await awaitedResponse.text() | ||
|
||
if (data.length === 0) { | ||
return "" | ||
} | ||
|
||
// JSON.parse() replacement with prototype poisoning protection. | ||
return json.parse(data) | ||
} | ||
|
||
return await awaitedResponse[_config.responseType]() | ||
} | ||
|
||
// Validate and handle responseType | ||
if (_config.responseType) { | ||
try { | ||
_response.data = await parseResponse() | ||
} catch (error) { | ||
// Handle parsing error for the specified responseType | ||
throw new TypeError( | ||
`Unsupported response type "${ | ||
_config.responseType | ||
}" specified in the request. The Content-Type of the response is "${response.headers.get( | ||
"Content-Type", | ||
)}".`, | ||
) | ||
} | ||
} | ||
|
||
return _response | ||
} | ||
|
||
const createHTTPMethods = (config?: Options): RequestMethods => { | ||
const httpShortcuts = {} | ||
|
||
/** | ||
* Build methods shortcut *Http.get().text()*. | ||
*/ | ||
methods.forEach((method) => { | ||
httpShortcuts[method] = (path: string, options?: MethodConfig) => { | ||
// If no responseType is specified, default to "json" | ||
let responseType = options?.responseType || "json" | ||
|
||
const responseHandlers = { | ||
...Object.fromEntries( | ||
Object.entries(responseTypes).map(([typeName, mimeType]) => [ | ||
typeName, | ||
() => { | ||
setHeaders((config.headers = config?.headers || {}), { | ||
accept: mimeType, | ||
}) | ||
|
||
responseType = typeName as ResponseType | ||
|
||
return responseHandlers | ||
}, | ||
]), | ||
), | ||
// https://javascript.plainenglish.io/the-benefit-of-the-thenable-object-in-javascript-78107b697211 | ||
then(...callback) { | ||
return httpAdapter(config, method, { path, responseType, ...options }).then( | ||
...callback, | ||
) | ||
}, | ||
catch(callback) { | ||
return responseHandlers.then().catch(callback) | ||
}, | ||
} | ||
|
||
return responseHandlers | ||
} | ||
}) | ||
|
||
return httpShortcuts as RequestMethods | ||
} | ||
|
||
/** | ||
* Creates hooks for a configuration object. | ||
* | ||
* @param {Object} config - The configuration object. | ||
* @returns {Object} An object containing hooks methods. | ||
* @throws {TypeError} Throws an error if a hook has already been invoked within the configuration. | ||
* | ||
* @example | ||
* const serviceConfig = {}; | ||
* const myHooks = createHooks(serviceConfig); | ||
*/ | ||
const createHooks = (config: Options) => { | ||
/** | ||
* @type {string[]} | ||
* Array of supported hook names. | ||
*/ | ||
const hooks = ["beforeRetry", "afterResponse", "beforeRequest"] | ||
|
||
return Object.fromEntries( | ||
hooks.map((hookName) => [ | ||
hookName, | ||
(fn) => { | ||
if (config?.hooks?.[hookName]) { | ||
throw new TypeError( | ||
`${hookName} has already been invoked within configuration.`, | ||
) | ||
} | ||
|
||
// Ensure that config.hooks is defined before accessing its properties | ||
Object.assign(config, { hooks: { ...(config.hooks || {}), [hookName]: fn } }) | ||
}, | ||
]), | ||
) | ||
} | ||
|
||
/** | ||
* Factory function for creating an HTTP client with configurable service instances. | ||
* | ||
* @function create | ||
* @param {Object} [config=null] - Configuration object for defining service instances with their respective URLs and headers. | ||
* @returns {Object} An HTTP client with service instances and utility functions. | ||
* @property {Object} {service} - Individual service instance with methods for making HTTP requests. | ||
* @property {Function} {service}.setHeaders - Sets headers for a specific service instance. | ||
* @property {Function} {service}.beforeRequest - Service-specific hook to edit request before making HTTP requests for a specific service. | ||
* | ||
* @example | ||
* const http = Nixify.create({ | ||
* github: { | ||
* url: "https://api.github.com", | ||
* headers: { | ||
* "x-API-KEY": "[TOKEN]" | ||
* } | ||
* }, | ||
* gitlab: { | ||
* url: "https://gitlab.com/api/v4/", | ||
* headers: {} | ||
* }, | ||
* }); | ||
* | ||
* // Set headers for a specific service instance | ||
* http.gitlab.setHeaders({ "authorization": `Bearer ${token}` }); | ||
* | ||
* // Set headers before making a request for a specific service instance | ||
* http.gitlab.beforeRequest(request => { | ||
* // Modify request headers or perform other actions | ||
* }); | ||
* | ||
* http.beforeRequest(request => { | ||
* // Modify request headers or perform other actions | ||
* }); | ||
* | ||
* await http.github.get('/search/repositories').json(); | ||
*/ | ||
const create = <T extends ServiceConfig>(config?: T) => { | ||
const instances = Object.fromEntries( | ||
Object.entries(config || { default: {} }).map(([service, serviceConfig]) => [ | ||
service, | ||
{ | ||
...createHTTPMethods(serviceConfig), | ||
...createHooks(serviceConfig), | ||
setHeaders: (newHeaders) => | ||
setHeaders((serviceConfig.headers = serviceConfig.headers || {}), newHeaders), | ||
}, | ||
]), | ||
) | ||
|
||
const forEachInstance = (arg) => Object.values(instances).forEach(arg) | ||
|
||
const resultingInstances = isEmpty(config) | ||
? { ...instances.default } | ||
: { | ||
...instances, | ||
...instances[Object.keys(instances)[0]], | ||
beforeRequest: (fn) => forEachInstance((instance) => instance.beforeRequest(fn)), | ||
afterResponse: (fn) => forEachInstance((instance) => instance.afterResponse(fn)), | ||
beforeRetry: (fn) => forEachInstance((instance) => instance.beforeRetry(fn)), | ||
setHeaders: (newHeaders) => | ||
forEachInstance((instance) => instance.setHeaders(newHeaders)), | ||
} | ||
|
||
return resultingInstances as XOR<ServiceReqMethods<T>, RequestMethods> | ||
} | ||
|
||
export default { create } |
Oops, something went wrong.