Skip to content

Commit

Permalink
Merge pull request #11 from FxOmar/feature/no-create
Browse files Browse the repository at this point in the history
Use Nixify without creating a new instance
  • Loading branch information
FxOmar authored Jan 12, 2024
2 parents 836fba4 + ff14f07 commit 4d65e09
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 273 deletions.
2 changes: 1 addition & 1 deletion @Nixify/chunk-ULYLNEH2.js → @Nixify/chunk-BOYYI7C6.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions @Nixify/chunk-K5WNYHMN.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions @Nixify/core/nixify.d.ts
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 };
8 changes: 8 additions & 0 deletions @Nixify/core/nixify.js
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';
6 changes: 3 additions & 3 deletions @Nixify/index.d.ts
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 };
13 changes: 7 additions & 6 deletions @Nixify/index.js
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 };
4 changes: 2 additions & 2 deletions @Nixify/utils/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ declare const mergeConfigs: (config: Options, methodConfig: MethodConfig, method
abortController: AbortController;
path?: string;
qs?: {
[name: string]: number | queryType;
} | {
readonly strict?: boolean;
readonly encode?: boolean;
readonly arrayFormat?: "bracket" | "index" | "comma" | "separator" | "bracket-separator" | "colon-list-separator" | "none";
readonly arrayFormatSeparator?: string;
readonly sort?: false | ((itemLeft: string, itemRight: string) => number);
readonly skipNull?: boolean;
readonly skipEmptyString?: boolean;
} | {
[name: string]: number | queryType;
};
json?: object;
responseType?: ResponseType;
Expand Down
4 changes: 2 additions & 2 deletions @Nixify/utils/index.js
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';
20 changes: 10 additions & 10 deletions __test__/http.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,19 @@ describe("Real-life data fetching 🗿.", () => {
expect(typeof data).toBe("object")
})

// it("Should fetch without creating new instance.", async () => {
// const { data, status } = await Nixify.get<string>(`${BASE_URL}/text`).text()
it("Should fetch without creating new instance.", async () => {
const { data, status } = await Nixify.get<string>(`${BASE_URL}/text`).text()

// expect(status).toBe(200)
// expect(data).toBe("Hello, world")
// })
expect(status).toBe(200)
expect(data).toBe("Hello, world")
})

// it("Should fetch data with the given interface.", async () => {
// const { data, status } = await Nixify.get<{ message: string }>(`${BASE_URL}/book`)
it("Should fetch data with the given interface.", async () => {
const { data, status } = await Nixify.get<{ message: string }>(`${BASE_URL}/book`)

// expect(status).toBe(200)
// expect(data.message).toBe("Hello, world")
// })
expect(status).toBe(200)
expect(data.message).toBe("Hello, world")
})

it("Should set header using function helper setHeader", async () => {
const http2 = Nixify.create({
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nixify",
"version": "3.1.1",
"version": "3.1.2",
"description": "Nixify is a tiny JavaScript HTTP client based on browser Fetch API.",
"type": "module",
"module": "@Nixify/index.js",
Expand Down
254 changes: 254 additions & 0 deletions src/core/nixify.ts
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 }
Loading

0 comments on commit 4d65e09

Please sign in to comment.