fetchja 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- var q=new Set(["__proto__","constructor","prototype"]);function w(e){if(Array.isArray(e))return e.map(r=>w(r));let t={type:e.type,id:e.id};for(let r in e.attributes)q.has(r)||(t[r]=e.attributes[r]);for(let r in e.relationships){if(q.has(r))continue;let n=e.relationships[r];n&&"data"in n&&(t[r]=n.data)}return t}function E(e){return typeof e=="object"&&e!==null}function z(e){let t={};if(e.data&&(t.data=w(e.data)),e.meta&&(t.meta=e.meta),!Array.isArray(e.included))return t;let r=new Map;for(let o of e.included){let d=w(o);r.set(`${o.type}:${o.id}`,d)}function n(o){return E(o)?r.get(`${o.type}:${o.id}`)??o:o}function s(o){return Array.isArray(o)?o.map(n):n(o)}function i(o){for(let d in o)E(o[d])&&(o[d]=s(o[d]))}for(let o of r.values())i(o);let{data:u}=t;return Array.isArray(u)?u.forEach(i):E(u)&&i(u),t}var h=class extends Error{status;statusText;errors;response;constructor(t,r={}){super(t),this.name="FetchjaError",this.status=r.status,this.statusText=r.statusText,this.errors=r.errors,this.response=r.response}};var I=new Set(["__proto__","constructor","prototype"]);function T(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)&&!(e instanceof Date)}function _(e,t,r){let n=[],s=new Set;function i(a){return r.pluralTypes(r.caseType(a))}function u(a,p){return a.type??i(p)}function o(a,p){return{type:u(a,p),id:String(a.id)}}function d(a,p){if(!T(a))return;if(a.id==null)throw new h("All included resources must have an ID.");let g=`${a.type??p}:${a.id}`;s.has(g)||(s.add(g),n.push(m(a,p)))}function m(a,p){let g={type:u(a,p)},c={},y={};for(let l in a){if(I.has(l)||l==="type")continue;if(l==="id"){g.id=String(a.id);continue}let f=a[l];if(Array.isArray(f)){c[l]={data:f.map(k=>(d(k,l),o(k,l)))};continue}if(T(f)){c[l]={data:o(f,l)},d(f,l);continue}y[l]=f}return Object.keys(y).length>0&&(g.attributes=y),Object.keys(c).length>0&&(g.relationships=c),g}let R=m(t,e);return JSON.stringify({data:R,included:n})}var M=new Set(["__proto__","constructor","prototype"]);function D(e){return typeof e=="object"&&e!==null&&!(e instanceof Date)}function F(e,t,r=""){let n=Array.isArray(t);for(let s in t){if(M.has(s))continue;let i=t[s],u=r?`${r}[${n?"":s}]`:s;D(i)?F(e,i,u):e.append(u,String(i))}}function P(e={}){let t=new URLSearchParams;return F(t,e),t}function J(e){return/^\d+$/.test(e)}function U(e,t){let r=e.split("/").filter(Boolean),n=r.reduce((s,i,u)=>J(i)?s:u,-1);return r.map((s,i)=>i===n?t.pluralize(t.resourceCase(s)):s).join("/")}function b(e,t){let r=e.split("/").filter(Boolean),n=r.pop()??"",s=r.join("/"),i=t.pluralize(t.resourceCase(n));return[n,s?`${s}/${i}`:i]}function x(e){return e.toLowerCase().replace(/[-_](.)/g,(t,r)=>r.toUpperCase()).replace(/^(.)/,t=>t.toLowerCase())}function j(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/_/g,"-").toLowerCase()}function A(e){return e.replace(/([a-z])([A-Z])/g,"$1_$2").replace(/-/g,"_").toLowerCase()}function C(e){return e===""?e:/us$/i.test(e)?`${e}es`:/s$/i.test(e)?e:/[^aeiou]y$/i.test(e)?e.replace(/y$/i,"ies"):/(x|z|ch|sh)$/i.test(e)?`${e}es`:`${e}s`}var S="application/vnd.api+json";function $(e){return e}var L=Object.assign(Object.create(null),{camel:x,kebab:j,snake:A,none:$});function H(e){return P(e)}function K(e){return e===!1?$:typeof e=="function"?e:C}function N(e){let t={};for(let[r,n]of e.headers.entries())t[r]=n;return t}var O=class{baseURL;headers;queryFormatter;typeCase;resourceCase;pluralize;onResponseError;#t;fetch;create;update;remove;constructor(t={}){this.baseURL=t.baseURL,this.headers={Accept:S,"Content-Type":S,...t.headers},this.#t=t.fetch,this.queryFormatter=typeof t.queryFormatter=="function"?t.queryFormatter:H,this.resourceCase=L[t.resourceCase??"none"]??$,this.typeCase=L[t.typeCase??"camel"]??x,this.pluralize=K(t.pluralize),this.onResponseError=t.onResponseError,this.fetch=this.get,this.create=this.post,this.update=this.patch,this.remove=this.delete}get#e(){return{resourceCase:this.resourceCase,pluralize:this.pluralize}}async request(t){let r=this.baseURL??t.baseURL;if(r===void 0||r==="")throw new h("A `baseURL` is required to make requests.");let n=t.url??"",s=n.startsWith("/")?n.slice(1):n,i=r.endsWith("/")?r:`${r}/`,u=new URL(s,i);t.params!==void 0&&(u.search=String(this.queryFormatter(t.params)));let o=t.body!==void 0&&t.type!==void 0?_(t.type,t.body,{caseType:this.typeCase,pluralTypes:this.pluralize}):void 0,d=this.#t??fetch,m=this.headers;function R(){let k=new Headers({...m,...t.headers}),v={method:t.method,headers:k,body:o};return d(u,v)}let a=await R(),p=Object.assign(a,{replayRequest:R}),g=p.ok||this.onResponseError===void 0?p:await this.onResponseError(p),c=g instanceof Response?g:p,y=N(c),f=(y["content-type"]??"").includes(S)?await c.json():{};if(!c.ok)throw new h(c.statusText||"Request failed",{status:c.status,statusText:c.statusText,errors:f.errors,response:c});return{...z(f),status:c.status,statusText:c.statusText,headers:y}}get(t,r={}){return this.request({...r,method:r.method??"GET",url:U(t,this.#e)})}post(t,r,n={}){let[s,i]=b(t,this.#e);return this.request({...n,method:n.method??"POST",url:i,body:r,type:s})}patch(t,r,n={}){let[s,i]=b(t,this.#e),u=r.id!==void 0?`${i}/${String(r.id)}`:i;return this.request({...n,method:n.method??"PATCH",url:u,body:r,type:s})}delete(t,r,n={}){let[,s]=b(t,this.#e);return this.request({...n,method:n.method??"DELETE",url:`${s}/${r}`})}};export{h as FetchjaError,x as camelCase,O as default,j as kebabCase,C as pluralize,A as snakeCase};
1
+ var q=new Set(["__proto__","constructor","prototype"]);function w(e){if(Array.isArray(e))return e.map(r=>w(r));let t={type:e.type,id:e.id};for(let r in e.attributes)q.has(r)||(t[r]=e.attributes[r]);for(let r in e.relationships){if(q.has(r))continue;let n=e.relationships[r];n&&"data"in n&&(t[r]=n.data)}return t}function O(e){return typeof e=="object"&&e!==null}function z(e){let t={};if(e.data&&(t.data=w(e.data)),e.meta&&(t.meta=e.meta),!Array.isArray(e.included))return t;let r=new Map;for(let i of e.included){let d=w(i);r.set(`${i.type}:${i.id}`,d)}function n(i){return O(i)?r.get(`${i.type}:${i.id}`)??i:i}function o(i){return Array.isArray(i)?i.map(n):n(i)}function s(i){for(let d in i)O(i[d])&&(i[d]=o(i[d]))}for(let i of r.values())s(i);let{data:u}=t;return Array.isArray(u)?u.forEach(s):O(u)&&s(u),t}var h=class extends Error{status;statusText;errors;response;constructor(t,r={}){super(t),this.name="FetchjaError",this.status=r.status,this.statusText=r.statusText,this.errors=r.errors,this.response=r.response}};var M=new Set(["__proto__","constructor","prototype"]);function T(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)&&!(e instanceof Date)}function _(e,t,r){let n=[],o=new Set;function s(a){return r.pluralTypes(r.caseType(a))}function u(a,p){return a.type??s(p)}function i(a,p){return{type:u(a,p),id:String(a.id)}}function d(a,p){if(!T(a))return;if(a.id==null)throw new h("All included resources must have an ID.");let g=`${a.type??p}:${a.id}`;o.has(g)||(o.add(g),n.push(m(a,p)))}function m(a,p){let g={type:u(a,p)},c={},y={};for(let l in a){if(M.has(l)||l==="type")continue;if(l==="id"){g.id=String(a.id);continue}let f=a[l];if(Array.isArray(f)){c[l]={data:f.map(k=>(d(k,l),i(k,l)))};continue}if(T(f)){c[l]={data:i(f,l)},d(f,l);continue}y[l]=f}return Object.keys(y).length>0&&(g.attributes=y),Object.keys(c).length>0&&(g.relationships=c),g}let R=m(t,e);return JSON.stringify({data:R,included:n})}var D=new Set(["__proto__","constructor","prototype"]);function F(e){return typeof e=="object"&&e!==null&&!(e instanceof Date)}function J(e){return Array.isArray(e)&&!e.some(F)}function P(e,t,r=""){let n=Array.isArray(t);for(let o in t){if(D.has(o))continue;let s=t[o],u=r?`${r}[${n?"":o}]`:o;J(s)&&s.length>0?e.append(u,s.map(String).join(",")):F(s)?P(e,s,u):e.append(u,String(s))}}function U(e={}){let t=new URLSearchParams;return P(t,e),t}function H(e){return/^\d+$/.test(e)}function L(e,t){let r=e.split("/").filter(Boolean),n=r.reduce((o,s,u)=>H(s)?o:u,-1);return r.map((o,s)=>s===n?t.pluralize(t.resourceCase(o)):o).join("/")}function b(e,t){let r=e.split("/").filter(Boolean),n=r.pop()??"",o=r.join("/"),s=t.pluralize(t.resourceCase(n));return[n,o?`${o}/${s}`:s]}function x(e){return e.toLowerCase().replace(/[-_](.)/g,(t,r)=>r.toUpperCase()).replace(/^(.)/,t=>t.toLowerCase())}function E(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/_/g,"-").toLowerCase()}function j(e){return e.replace(/([a-z])([A-Z])/g,"$1_$2").replace(/-/g,"_").toLowerCase()}function S(e){return e===""?e:/us$/i.test(e)?`${e}es`:/s$/i.test(e)?e:/[^aeiou]y$/i.test(e)?e.replace(/y$/i,"ies"):/(x|z|ch|sh)$/i.test(e)?`${e}es`:`${e}s`}var C="application/vnd.api+json";function $(e){return e}var v=Object.assign(Object.create(null),{camel:x,kebab:E,snake:j,none:$});function K(e){return U(e)}function N(e){return e===!1?$:typeof e=="function"?e:S}function G(e){let t={};for(let[r,n]of e.headers.entries())t[r]=n;return t}var A=class{baseURL;headers;queryFormatter;typeCase;resourceCase;pluralize;onResponseError;#t;fetch;create;update;remove;constructor(t={}){this.baseURL=t.baseURL,this.headers={Accept:C,"Content-Type":C,...t.headers},this.#t=t.fetch,this.queryFormatter=typeof t.queryFormatter=="function"?t.queryFormatter:K,this.resourceCase=v[t.resourceCase??"none"]??$,this.typeCase=v[t.typeCase??"camel"]??x,this.pluralize=N(t.pluralize),this.onResponseError=t.onResponseError,this.fetch=this.get,this.create=this.post,this.update=this.patch,this.remove=this.delete}get#e(){return{resourceCase:this.resourceCase,pluralize:this.pluralize}}async request(t){let r=this.baseURL??t.baseURL;if(r===void 0||r==="")throw new h("A `baseURL` is required to make requests.");let n=t.url??"",o=n.startsWith("/")?n.slice(1):n,s=r.endsWith("/")?r:`${r}/`,u=new URL(o,s);t.params!==void 0&&(u.search=String(this.queryFormatter(t.params)));let i=t.body!==void 0&&t.type!==void 0?_(t.type,t.body,{caseType:this.typeCase,pluralTypes:this.pluralize}):void 0,d=this.#t??fetch,m=this.headers;function R(){let k=new Headers({...m,...t.headers}),I={method:t.method,headers:k,body:i};return d(u,I)}let a=await R(),p=Object.assign(a,{replayRequest:R}),g=p.ok||this.onResponseError===void 0?p:await this.onResponseError(p),c=g instanceof Response?g:p,y=G(c),f=(y["content-type"]??"").includes(C)?await c.json():{};if(!c.ok)throw new h(c.statusText||"Request failed",{status:c.status,statusText:c.statusText,errors:f.errors,response:c});return{...z(f),status:c.status,statusText:c.statusText,headers:y}}get(t,r={}){return this.request({...r,method:r.method??"GET",url:L(t,this.#e)})}post(t,r,n={}){let[o,s]=b(t,this.#e);return this.request({...n,method:n.method??"POST",url:s,body:r,type:o})}patch(t,r,n={}){let[o,s]=b(t,this.#e),u=r.id!==void 0?`${s}/${String(r.id)}`:s;return this.request({...n,method:n.method??"PATCH",url:u,body:r,type:o})}delete(t,r,n={}){let[,o]=b(t,this.#e);return this.request({...n,method:n.method??"DELETE",url:`${o}/${r}`})}};export{h as FetchjaError,x as camelCase,A as default,E as kebabCase,S as pluralize,j as snakeCase};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/deattribute.ts","../src/deserialize.ts","../src/errors.ts","../src/serialize.ts","../src/query.ts","../src/model.ts","../src/case.ts","../src/pluralize.ts","../src/client.ts"],"sourcesContent":["/**\n * Property names that could pollute an object's prototype. They are\n * skipped whenever data from a response is copied into a new object.\n */\nconst DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n/**\n * A single JSON:API resource object.\n */\nexport interface Resource {\n /** The resource type. */\n type?: string\n\n /** The resource id. */\n id?: string\n\n /** The resource attributes. */\n attributes?: Record<string, unknown>\n\n /** The resource relationships. */\n relationships?: Record<string, { data?: unknown }>\n}\n\n/**\n * Flatten a JSON:API resource (or array of resources) by lifting its\n * `attributes` and `relationships` onto the top level, next to `type`\n * and `id`.\n *\n * @param data - The resource or resources to flatten.\n * @returns The flattened object or array of objects.\n */\nexport function deattribute (\n data: Resource | Resource[]\n): Record<string, unknown> | Record<string, unknown>[] {\n if (Array.isArray(data)) {\n return data.map(item => deattribute(item) as Record<string, unknown>)\n }\n\n const output: Record<string, unknown> = {\n type: data.type,\n id: data.id\n }\n\n for (const key in data.attributes) {\n if (DANGEROUS_KEYS.has(key)) {\n continue\n }\n\n output[key] = data.attributes[key]\n }\n\n for (const key in data.relationships) {\n if (DANGEROUS_KEYS.has(key)) {\n continue\n }\n\n const relation = data.relationships[key]\n\n if (relation && 'data' in relation) {\n output[key] = relation.data\n }\n }\n\n return output\n}\n","import { deattribute } from './deattribute.js'\n\n/**\n * Check whether a value is a non-null object.\n *\n * @param value - The value to check.\n * @returns `true` when the value is a non-null object.\n */\nfunction isObject (value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n\n/**\n * Deserialize a JSON:API response document into a flat object. Resources\n * found in `included` are resolved (by `type` and `id`) straight into\n * the relationships that reference them.\n *\n * @param response - The JSON:API document to deserialize.\n * @returns The flattened response, with `data` and optional `meta`.\n */\nexport function deserialize (\n response: Record<string, any>\n): Record<string, unknown> {\n const output: Record<string, unknown> = {}\n\n if (response.data) {\n output.data = deattribute(response.data)\n }\n\n if (response.meta) {\n output.meta = response.meta\n }\n\n if (!Array.isArray(response.included)) {\n return output\n }\n\n const resourcesByKey = new Map<string, Record<string, unknown>>()\n\n for (const resource of response.included) {\n const flat = deattribute(resource) as Record<string, unknown>\n\n resourcesByKey.set(`${resource.type}:${resource.id}`, flat)\n }\n\n /**\n * Replace a resource identifier with its full resource, when known.\n *\n * @param reference - The identifier or value to resolve.\n * @returns The resolved resource, or the value unchanged.\n */\n function resolve (reference: unknown): unknown {\n if (!isObject(reference)) {\n return reference\n }\n\n return resourcesByKey.get(`${reference.type}:${reference.id}`) ??\n reference\n }\n\n /**\n * Resolve a relationship value, mapping over to-many arrays.\n *\n * @param value - The relationship value to resolve.\n * @returns The resolved value.\n */\n function replace (value: unknown): unknown {\n return Array.isArray(value) ? value.map(resolve) : resolve(value)\n }\n\n /**\n * Resolve every relationship reference on a flattened resource.\n *\n * @param entry - The flattened resource to link.\n */\n function link (entry: Record<string, unknown>): void {\n for (const key in entry) {\n if (isObject(entry[key])) {\n entry[key] = replace(entry[key])\n }\n }\n }\n\n for (const resource of resourcesByKey.values()) {\n link(resource)\n }\n\n const { data } = output\n\n if (Array.isArray(data)) {\n data.forEach(link)\n } else if (isObject(data)) {\n link(data)\n }\n\n return output\n}\n","/**\n * A single error object as defined by the JSON:API specification.\n *\n * @see https://jsonapi.org/format/#error-objects\n */\nexport interface JsonApiError {\n /** The HTTP status code, as a string. */\n status?: string\n\n /** An application-specific error code. */\n code?: string\n\n /** A short, human-readable summary of the problem. */\n title?: string\n\n /** A human-readable explanation of this occurrence of the problem. */\n detail?: string\n\n /** References to the source of the error. */\n source?: Record<string, unknown>\n\n [key: string]: unknown\n}\n\n/**\n * The extra fields used to build a {@link FetchjaError}.\n */\nexport interface FetchjaErrorInit {\n /** The HTTP status code of the failed response. */\n status?: number\n\n /** The HTTP status text of the failed response. */\n statusText?: string\n\n /** The JSON:API error objects returned by the server. */\n errors?: JsonApiError[]\n\n /** The raw failed response. */\n response?: Response\n}\n\n/**\n * The error thrown when a request fails. It carries the HTTP status and\n * any JSON:API error objects returned by the server.\n */\nexport class FetchjaError extends Error {\n /** The HTTP status code of the failed response. */\n status?: number\n\n /** The HTTP status text of the failed response. */\n statusText?: string\n\n /** The JSON:API error objects returned by the server. */\n errors?: JsonApiError[]\n\n /** The raw failed response. */\n response?: Response\n\n /**\n * @param message - The error message.\n * @param init - The extra fields to attach to the error.\n */\n constructor (message: string, init: FetchjaErrorInit = {}) {\n super(message)\n\n this.name = 'FetchjaError'\n this.status = init.status\n this.statusText = init.statusText\n this.errors = init.errors\n this.response = init.response\n }\n}\n","import { FetchjaError } from './errors.js'\n\n/**\n * Property names that could pollute an object's prototype. They are\n * skipped while reading the input object.\n */\nconst DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n/**\n * The transforms applied to resource `type` names while serializing.\n */\nexport interface SerializeOptions {\n /** Cases a `type` name. */\n caseType: (type: string) => string\n\n /** Pluralizes a `type` name. */\n pluralTypes: (type: string) => string\n}\n\n/**\n * Check whether a value is a plain object. Arrays, `null`, and `Date`\n * instances are treated as values, not relationships.\n *\n * @param value - The value to check.\n * @returns `true` when the value is a plain object.\n */\nfunction isPlainObject (\n value: unknown\n): value is Record<string, unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n )\n}\n\n/**\n * Serialize a plain object into a JSON:API request document. Nested\n * objects with an `id` become relationships, and the full resources are\n * collected into the top-level `included` array.\n *\n * @param type - The resource type of the root object.\n * @param input - The plain object to serialize.\n * @param options - The type-name transforms.\n * @returns The JSON:API document as a JSON string.\n */\nexport function serialize (\n type: string,\n input: Record<string, unknown>,\n options: SerializeOptions\n): string {\n const included: Record<string, unknown>[] = []\n const includedKeys = new Set<string>()\n\n /**\n * Apply the configured type-name transforms.\n *\n * @param rawType - The raw type name.\n * @returns The transformed type name.\n */\n function formatType (rawType: string): string {\n return options.pluralTypes(options.caseType(rawType))\n }\n\n /**\n * Resolve a resource's `type`: its own `type` when present, otherwise\n * one derived from the relationship key. Used for both the identifier\n * and the included resource, so the two always match.\n *\n * @param node - The resource.\n * @param fallbackType - The relationship key to fall back to.\n * @returns The resource type.\n */\n function resourceType (\n node: Record<string, unknown>,\n fallbackType: string\n ): string {\n return (node.type as string) ?? formatType(fallbackType)\n }\n\n /**\n * Build a JSON:API resource identifier (`{ type, id }`).\n *\n * @param resource - The related resource.\n * @param fallbackType - The type to use when the resource has none.\n * @returns The resource identifier.\n */\n function toIdentifier (\n resource: Record<string, unknown>,\n fallbackType: string\n ): Record<string, unknown> {\n return {\n type: resourceType(resource, fallbackType),\n id: String(resource.id)\n }\n }\n\n /**\n * Collect a related resource into `included`, de-duplicated by its\n * `type` and `id`.\n *\n * @param resource - The related resource.\n * @param fallbackType - The type to use when the resource has none.\n */\n function collectIncluded (\n resource: unknown,\n fallbackType: string\n ): void {\n if (!isPlainObject(resource)) {\n return\n }\n\n if (resource.id == null) {\n throw new FetchjaError('All included resources must have an ID.')\n }\n\n const key = `${resource.type ?? fallbackType}:${resource.id}`\n\n if (includedKeys.has(key)) {\n return\n }\n\n includedKeys.add(key)\n included.push(extractResource(resource, fallbackType))\n }\n\n /**\n * Build a JSON:API resource object from a plain object, splitting its\n * fields into `attributes` and `relationships`.\n *\n * @param node - The plain object to convert.\n * @param rawType - The raw type name of the resource.\n * @returns The JSON:API resource object.\n */\n function extractResource (\n node: Record<string, unknown>,\n rawType: string\n ): Record<string, unknown> {\n const data: Record<string, unknown> = {\n type: resourceType(node, rawType)\n }\n const relationships: Record<string, unknown> = {}\n const attributes: Record<string, unknown> = {}\n\n for (const key in node) {\n if (DANGEROUS_KEYS.has(key) || key === 'type') {\n continue\n }\n\n if (key === 'id') {\n data.id = String(node.id)\n\n continue\n }\n\n const value = node[key]\n\n if (Array.isArray(value)) {\n relationships[key] = {\n data: value.map(item => {\n collectIncluded(item, key)\n\n return toIdentifier(item, key)\n })\n }\n\n continue\n }\n\n if (isPlainObject(value)) {\n relationships[key] = { data: toIdentifier(value, key) }\n collectIncluded(value, key)\n\n continue\n }\n\n attributes[key] = value\n }\n\n if (Object.keys(attributes).length > 0) {\n data.attributes = attributes\n }\n\n if (Object.keys(relationships).length > 0) {\n data.relationships = relationships\n }\n\n return data\n }\n\n const data = extractResource(input, type)\n\n return JSON.stringify({ data, included })\n}\n","/**\n * Property names that could pollute an object's prototype. They are\n * skipped while reading the parameters object.\n */\nconst DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n/**\n * Check whether a value should be walked into when building the query.\n * Arrays and plain objects are traversable; `Date` instances are not.\n *\n * @param value - The value to check.\n * @returns `true` when the value should be traversed.\n */\nfunction isTraversable (value: unknown): value is object {\n return (\n typeof value === 'object' &&\n value !== null &&\n !(value instanceof Date)\n )\n}\n\n/**\n * Append an object's entries to a query, recursing into nested objects\n * and arrays using JSON:API-friendly bracket notation.\n *\n * @param query - The query to append to.\n * @param object - The object or array to walk.\n * @param prefix - The key prefix built from parent keys.\n */\nfunction buildQuery (\n query: URLSearchParams,\n object: object,\n prefix = ''\n): void {\n const isArray = Array.isArray(object)\n\n for (const key in object) {\n if (DANGEROUS_KEYS.has(key)) {\n continue\n }\n\n const value = (object as Record<string, unknown>)[key]\n const path = prefix ? `${prefix}[${isArray ? '' : key}]` : key\n\n if (isTraversable(value)) {\n buildQuery(query, value, path)\n } else {\n query.append(path, String(value))\n }\n }\n}\n\n/**\n * Serialize a plain object into URL query parameters.\n *\n * @param parameters - The parameters to serialize.\n * @returns The serialized query parameters.\n */\nexport function queryFormatter (\n parameters: Record<string, unknown> = {}\n): URLSearchParams {\n const query = new URLSearchParams()\n\n buildQuery(query, parameters)\n\n return query\n}\n","/**\n * The transforms used to turn a model name into a URL path segment.\n */\nexport interface ModelOptions {\n /** Cases a single path segment. */\n resourceCase: (segment: string) => string\n\n /** Pluralizes a single path segment. */\n pluralize: (segment: string) => string\n}\n\n/**\n * Check whether a path segment is a numeric resource id.\n *\n * @param segment - The path segment to check.\n * @returns `true` when the segment is all digits.\n */\nfunction isResourceId (segment: string): boolean {\n return /^\\d+$/.test(segment)\n}\n\n/**\n * Normalize a `get` model into a URL path. Only the resource being\n * addressed (the last non-numeric segment) is cased and pluralized, so\n * namespaces and numeric ids are left untouched. This keeps `get`\n * consistent with the path built by {@link splitModel}.\n *\n * @param model - The model path, e.g. `article`, `articles/1`.\n * @param options - The casing and pluralization transforms.\n * @returns The normalized URL path.\n */\nexport function normalizePath (\n model: string,\n options: ModelOptions\n): string {\n const segments = model.split('/').filter(Boolean)\n\n const targetIndex = segments.reduce(\n (last, segment, index) => (isResourceId(segment) ? last : index),\n -1\n )\n\n return segments\n .map((segment, index) =>\n index === targetIndex\n ? options.pluralize(options.resourceCase(segment))\n : segment\n )\n .join('/')\n}\n\n/**\n * Split a write model into its resource `type` and URL path. The last\n * segment is treated as the resource and is cased and pluralized.\n *\n * @param url - The model path, e.g. `article`, `admin/article`.\n * @param options - The casing and pluralization transforms.\n * @returns A `[type, path]` tuple.\n */\nexport function splitModel (\n url: string,\n options: ModelOptions\n): [string, string] {\n const parts = url.split('/').filter(Boolean)\n const model = parts.pop() ?? ''\n const namespace = parts.join('/')\n const path = options.pluralize(options.resourceCase(model))\n\n return [model, namespace ? `${namespace}/${path}` : path]\n}\n","/**\n * Convert a string to `camelCase` from `snake_case`, `kebab-case`, or\n * `SCREAMING_SNAKE_CASE`.\n *\n * @param input - The string to convert.\n * @returns The converted string.\n */\nexport function camelCase (input: string): string {\n return input\n .toLowerCase()\n .replace(/[-_](.)/g, (_match, character: string) =>\n character.toUpperCase()\n )\n .replace(/^(.)/, character => character.toLowerCase())\n}\n\n/**\n * Convert a string to `kebab-case` from `camelCase` or `snake_case`.\n *\n * @param input - The string to convert.\n * @returns The converted string.\n */\nexport function kebabCase (input: string): string {\n return input\n .replace(/([a-z])([A-Z])/g, '$1-$2')\n .replace(/_/g, '-')\n .toLowerCase()\n}\n\n/**\n * Convert a string to `snake_case` from `camelCase` or `kebab-case`.\n *\n * @param input - The string to convert.\n * @returns The converted string.\n */\nexport function snakeCase (input: string): string {\n return input\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n .replace(/-/g, '_')\n .toLowerCase()\n}\n","/**\n * Pluralize an English word using a small set of common rules.\n *\n * This is a lightweight, dependency-free helper that covers the cases\n * most JSON:API resource names need. It is intentionally simple and\n * idempotent: a word that already looks plural is returned unchanged.\n * For irregular words (such as `person` becoming `people`), inject a\n * fuller implementation through the `pluralize` option.\n *\n * @param word - The word to pluralize.\n * @returns The pluralized word.\n */\nexport function pluralize (word: string): string {\n if (word === '') {\n return word\n }\n\n // Singular words ending in \"us\" take \"es\": status -> statuses,\n // bus -> buses, virus -> viruses.\n if (/us$/i.test(word)) {\n return `${word}es`\n }\n\n // Anything else ending in \"s\" is treated as already plural.\n if (/s$/i.test(word)) {\n return word\n }\n\n // A consonant followed by \"y\" becomes \"ies\": category -> categories.\n if (/[^aeiou]y$/i.test(word)) {\n return word.replace(/y$/i, 'ies')\n }\n\n // Sibilant endings take \"es\": box -> boxes, match -> matches.\n if (/(x|z|ch|sh)$/i.test(word)) {\n return `${word}es`\n }\n\n return `${word}s`\n}\n","import { deserialize } from './deserialize.js'\nimport { serialize } from './serialize.js'\nimport { queryFormatter } from './query.js'\nimport { normalizePath, splitModel, type ModelOptions } from './model.js'\nimport { camelCase, kebabCase, snakeCase } from './case.js'\nimport { pluralize as defaultPluralize } from './pluralize.js'\nimport { FetchjaError } from './errors.js'\nimport type { FetchjaOptions, RequestOptions } from './types.js'\n\n/** The media type required by the JSON:API specification. */\nconst JSON_API_MEDIA_TYPE = 'application/vnd.api+json'\n\n/**\n * Return the given string unchanged. Used as the default transform when\n * no resource casing or pluralization is requested.\n *\n * @param value - The string to return.\n * @returns The same string.\n */\nfunction identity (value: string): string {\n return value\n}\n\n/**\n * The resource-casing strategies selectable through `resourceCase`.\n * Stored on a null-prototype object so an unexpected option value cannot\n * reach inherited properties.\n */\nconst RESOURCE_CASES: Record<string, (value: string) => string> =\n Object.assign(Object.create(null), {\n camel: camelCase,\n kebab: kebabCase,\n snake: snakeCase,\n none: identity\n })\n\n/**\n * The default query serializer, used when no `queryFormatter` option is\n * provided.\n *\n * @param params - The query parameters to serialize.\n * @returns The serialized query parameters.\n */\nfunction defaultQueryFormatter (\n params: unknown\n): string | URLSearchParams {\n return queryFormatter(params as Record<string, unknown>)\n}\n\n/**\n * Resolve the `pluralize` option into a transform function.\n *\n * @param option - The `pluralize` option value.\n * @returns The pluralization transform.\n */\nfunction resolvePluralizer (\n option: FetchjaOptions['pluralize']\n): (value: string) => string {\n if (option === false) {\n return identity\n }\n\n if (typeof option === 'function') {\n return option\n }\n\n return defaultPluralize\n}\n\n/**\n * Copy every response header into a plain object.\n *\n * @param response - The response to read headers from.\n * @returns A plain object of header names to values.\n */\nfunction collectHeaders (response: Response): Record<string, string> {\n const headers: Record<string, string> = {}\n\n for (const [key, value] of response.headers.entries()) {\n headers[key] = value\n }\n\n return headers\n}\n\n/**\n * A super simple, lightweight JSON:API client built on the Fetch API.\n */\nexport default class Fetchja {\n /** The base URL prepended to every request. */\n baseURL?: string\n\n /** Headers merged into every request. */\n headers: Record<string, string>\n\n /** Serializes the request query parameters. */\n queryFormatter: (params: unknown) => string | URLSearchParams\n\n /** Cases `type` names when serializing request bodies. */\n typeCase: (value: string) => string\n\n /** Cases resource names in the URL path. */\n resourceCase: (value: string) => string\n\n /** Pluralizes resource names. */\n pluralize: (value: string) => string\n\n /** Interceptor invoked on a non-OK response, before throwing. */\n onResponseError: FetchjaOptions['onResponseError']\n\n /** The custom fetch implementation, when provided. */\n readonly #fetch?: typeof fetch\n\n /** Alias of {@link Fetchja.get}. */\n fetch!: Fetchja['get']\n\n /** Alias of {@link Fetchja.post}. */\n create!: Fetchja['post']\n\n /** Alias of {@link Fetchja.patch}. */\n update!: Fetchja['patch']\n\n /** Alias of {@link Fetchja.delete}. */\n remove!: Fetchja['delete']\n\n /**\n * @param options - The client options.\n */\n constructor (options: FetchjaOptions = {}) {\n this.baseURL = options.baseURL\n\n this.headers = {\n 'Accept': JSON_API_MEDIA_TYPE,\n 'Content-Type': JSON_API_MEDIA_TYPE,\n ...options.headers\n }\n\n this.#fetch = options.fetch\n\n this.queryFormatter = typeof options.queryFormatter === 'function'\n ? options.queryFormatter\n : defaultQueryFormatter\n\n this.resourceCase =\n RESOURCE_CASES[options.resourceCase ?? 'none'] ?? identity\n\n this.typeCase =\n RESOURCE_CASES[options.typeCase ?? 'camel'] ?? camelCase\n\n this.pluralize = resolvePluralizer(options.pluralize)\n\n this.onResponseError = options.onResponseError\n\n this.fetch = this.get\n this.create = this.post\n this.update = this.patch\n this.remove = this.delete\n }\n\n /** The transforms shared by the path-building helpers. */\n get #modelOptions (): ModelOptions {\n return {\n resourceCase: this.resourceCase,\n pluralize: this.pluralize\n }\n }\n\n /**\n * Perform a request and return the deserialized response. Throws a\n * {@link FetchjaError} on a non-OK response.\n *\n * @param options - The request options.\n * @returns The deserialized response.\n */\n async request (\n options: RequestOptions\n ): Promise<Record<string, unknown>> {\n const base = this.baseURL ?? options.baseURL\n\n if (base === undefined || base === '') {\n throw new FetchjaError(\n 'A `baseURL` is required to make requests.'\n )\n }\n\n const requestPath = options.url ?? ''\n\n const relativePath = requestPath.startsWith('/')\n ? requestPath.slice(1)\n : requestPath\n\n const baseWithSlash = base.endsWith('/') ? base : `${base}/`\n const url = new URL(relativePath, baseWithSlash)\n\n if (options.params !== undefined) {\n url.search = String(this.queryFormatter(options.params))\n }\n\n const body = options.body !== undefined && options.type !== undefined\n ? serialize(options.type, options.body, {\n caseType: this.typeCase,\n pluralTypes: this.pluralize\n })\n : undefined\n\n const requestFetch = this.#fetch ?? fetch\n const baseHeaders = this.headers\n\n /**\n * Send the request. Defined as a closure so it can be replayed by\n * the `onResponseError` interceptor with the current headers.\n *\n * @returns The raw response.\n */\n function sendRequest (): Promise<Response> {\n const headers = new Headers({\n ...baseHeaders,\n ...options.headers\n })\n\n const init: RequestInit = {\n method: options.method,\n headers,\n body\n }\n\n return requestFetch(url, init)\n }\n\n const initialResponse = await sendRequest()\n\n const augmentedResponse = Object.assign(initialResponse, {\n replayRequest: sendRequest\n })\n\n const handledResponse =\n augmentedResponse.ok || this.onResponseError === undefined\n ? augmentedResponse\n : await this.onResponseError(augmentedResponse)\n\n const response = handledResponse instanceof Response\n ? handledResponse\n : augmentedResponse\n\n const responseHeaders = collectHeaders(response)\n const contentType = responseHeaders['content-type'] ?? ''\n\n const payload = contentType.includes(JSON_API_MEDIA_TYPE)\n ? await response.json()\n : {}\n\n if (!response.ok) {\n throw new FetchjaError(response.statusText || 'Request failed', {\n status: response.status,\n statusText: response.statusText,\n errors: payload.errors,\n response\n })\n }\n\n return {\n ...deserialize(payload),\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders\n }\n }\n\n /**\n * Read one or more resources.\n *\n * @param model - The resource path, e.g. `articles` or `articles/1`.\n * @param options - Extra request options.\n * @returns The deserialized response.\n */\n get (\n model: string,\n options: RequestOptions = {}\n ): Promise<Record<string, unknown>> {\n return this.request({\n ...options,\n method: options.method ?? 'GET',\n url: normalizePath(model, this.#modelOptions)\n })\n }\n\n /**\n * Create a resource.\n *\n * @param model - The resource name, e.g. `article`.\n * @param body - The resource to create.\n * @param options - Extra request options.\n * @returns The deserialized response.\n */\n post (\n model: string,\n body: Record<string, unknown>,\n options: RequestOptions = {}\n ): Promise<Record<string, unknown>> {\n const [type, url] = splitModel(model, this.#modelOptions)\n\n return this.request({\n ...options,\n method: options.method ?? 'POST',\n url,\n body,\n type\n })\n }\n\n /**\n * Update a resource. When the body has an `id`, it is appended to the\n * URL.\n *\n * @param model - The resource name, e.g. `article`.\n * @param body - The fields to update.\n * @param options - Extra request options.\n * @returns The deserialized response.\n */\n patch (\n model: string,\n body: Record<string, unknown>,\n options: RequestOptions = {}\n ): Promise<Record<string, unknown>> {\n const [type, url] = splitModel(model, this.#modelOptions)\n\n const resourceUrl = body.id !== undefined\n ? `${url}/${String(body.id)}`\n : url\n\n return this.request({\n ...options,\n method: options.method ?? 'PATCH',\n url: resourceUrl,\n body,\n type\n })\n }\n\n /**\n * Delete a resource. No request body is sent.\n *\n * @param model - The resource name, e.g. `article`.\n * @param id - The id of the resource to delete.\n * @param options - Extra request options.\n * @returns The deserialized response.\n */\n delete (\n model: string,\n id: string,\n options: RequestOptions = {}\n ): Promise<Record<string, unknown>> {\n const [, url] = splitModel(model, this.#modelOptions)\n\n return this.request({\n ...options,\n method: options.method ?? 'DELETE',\n url: `${url}/${id}`\n })\n }\n}\n"],"mappings":"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","names":["DANGEROUS_KEYS","deattribute","data","item","output","key","relation","isObject","value","deserialize","response","output","deattribute","resourcesByKey","resource","flat","resolve","reference","replace","link","entry","key","data","FetchjaError","message","init","DANGEROUS_KEYS","isPlainObject","value","serialize","type","input","options","included","includedKeys","formatType","rawType","resourceType","node","fallbackType","toIdentifier","resource","collectIncluded","FetchjaError","key","extractResource","data","relationships","attributes","item","DANGEROUS_KEYS","isTraversable","value","buildQuery","query","object","prefix","isArray","key","path","queryFormatter","parameters","isResourceId","segment","normalizePath","model","options","segments","targetIndex","last","index","splitModel","url","parts","namespace","path","camelCase","input","_match","character","kebabCase","snakeCase","pluralize","word","JSON_API_MEDIA_TYPE","identity","value","RESOURCE_CASES","camelCase","kebabCase","snakeCase","defaultQueryFormatter","params","queryFormatter","resolvePluralizer","option","pluralize","collectHeaders","response","headers","key","Fetchja","#fetch","options","#modelOptions","base","FetchjaError","requestPath","relativePath","baseWithSlash","url","body","serialize","requestFetch","baseHeaders","sendRequest","init","initialResponse","augmentedResponse","handledResponse","responseHeaders","payload","deserialize","model","normalizePath","type","splitModel","resourceUrl","id"]}
1
+ {"version":3,"sources":["../src/deattribute.ts","../src/deserialize.ts","../src/errors.ts","../src/serialize.ts","../src/query.ts","../src/model.ts","../src/case.ts","../src/pluralize.ts","../src/client.ts"],"sourcesContent":["/**\n * Property names that could pollute an object's prototype. They are\n * skipped whenever data from a response is copied into a new object.\n */\nconst DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n/**\n * A single JSON:API resource object.\n */\nexport interface Resource {\n /** The resource type. */\n type?: string\n\n /** The resource id. */\n id?: string\n\n /** The resource attributes. */\n attributes?: Record<string, unknown>\n\n /** The resource relationships. */\n relationships?: Record<string, { data?: unknown }>\n}\n\n/**\n * Flatten a JSON:API resource (or array of resources) by lifting its\n * `attributes` and `relationships` onto the top level, next to `type`\n * and `id`.\n *\n * @param data - The resource or resources to flatten.\n * @returns The flattened object or array of objects.\n */\nexport function deattribute (\n data: Resource | Resource[]\n): Record<string, unknown> | Record<string, unknown>[] {\n if (Array.isArray(data)) {\n return data.map(item => deattribute(item) as Record<string, unknown>)\n }\n\n const output: Record<string, unknown> = {\n type: data.type,\n id: data.id\n }\n\n for (const key in data.attributes) {\n if (DANGEROUS_KEYS.has(key)) {\n continue\n }\n\n output[key] = data.attributes[key]\n }\n\n for (const key in data.relationships) {\n if (DANGEROUS_KEYS.has(key)) {\n continue\n }\n\n const relation = data.relationships[key]\n\n if (relation && 'data' in relation) {\n output[key] = relation.data\n }\n }\n\n return output\n}\n","import { deattribute } from './deattribute.js'\n\n/**\n * Check whether a value is a non-null object.\n *\n * @param value - The value to check.\n * @returns `true` when the value is a non-null object.\n */\nfunction isObject (value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n\n/**\n * Deserialize a JSON:API response document into a flat object. Resources\n * found in `included` are resolved (by `type` and `id`) straight into\n * the relationships that reference them.\n *\n * @param response - The JSON:API document to deserialize.\n * @returns The flattened response, with `data` and optional `meta`.\n */\nexport function deserialize (\n response: Record<string, any>\n): Record<string, unknown> {\n const output: Record<string, unknown> = {}\n\n if (response.data) {\n output.data = deattribute(response.data)\n }\n\n if (response.meta) {\n output.meta = response.meta\n }\n\n if (!Array.isArray(response.included)) {\n return output\n }\n\n const resourcesByKey = new Map<string, Record<string, unknown>>()\n\n for (const resource of response.included) {\n const flat = deattribute(resource) as Record<string, unknown>\n\n resourcesByKey.set(`${resource.type}:${resource.id}`, flat)\n }\n\n /**\n * Replace a resource identifier with its full resource, when known.\n *\n * @param reference - The identifier or value to resolve.\n * @returns The resolved resource, or the value unchanged.\n */\n function resolve (reference: unknown): unknown {\n if (!isObject(reference)) {\n return reference\n }\n\n return resourcesByKey.get(`${reference.type}:${reference.id}`) ??\n reference\n }\n\n /**\n * Resolve a relationship value, mapping over to-many arrays.\n *\n * @param value - The relationship value to resolve.\n * @returns The resolved value.\n */\n function replace (value: unknown): unknown {\n return Array.isArray(value) ? value.map(resolve) : resolve(value)\n }\n\n /**\n * Resolve every relationship reference on a flattened resource.\n *\n * @param entry - The flattened resource to link.\n */\n function link (entry: Record<string, unknown>): void {\n for (const key in entry) {\n if (isObject(entry[key])) {\n entry[key] = replace(entry[key])\n }\n }\n }\n\n for (const resource of resourcesByKey.values()) {\n link(resource)\n }\n\n const { data } = output\n\n if (Array.isArray(data)) {\n data.forEach(link)\n } else if (isObject(data)) {\n link(data)\n }\n\n return output\n}\n","/**\n * A single error object as defined by the JSON:API specification.\n *\n * @see https://jsonapi.org/format/#error-objects\n */\nexport interface JsonApiError {\n /** The HTTP status code, as a string. */\n status?: string\n\n /** An application-specific error code. */\n code?: string\n\n /** A short, human-readable summary of the problem. */\n title?: string\n\n /** A human-readable explanation of this occurrence of the problem. */\n detail?: string\n\n /** References to the source of the error. */\n source?: Record<string, unknown>\n\n [key: string]: unknown\n}\n\n/**\n * The extra fields used to build a {@link FetchjaError}.\n */\nexport interface FetchjaErrorInit {\n /** The HTTP status code of the failed response. */\n status?: number\n\n /** The HTTP status text of the failed response. */\n statusText?: string\n\n /** The JSON:API error objects returned by the server. */\n errors?: JsonApiError[]\n\n /** The raw failed response. */\n response?: Response\n}\n\n/**\n * The error thrown when a request fails. It carries the HTTP status and\n * any JSON:API error objects returned by the server.\n */\nexport class FetchjaError extends Error {\n /** The HTTP status code of the failed response. */\n status?: number\n\n /** The HTTP status text of the failed response. */\n statusText?: string\n\n /** The JSON:API error objects returned by the server. */\n errors?: JsonApiError[]\n\n /** The raw failed response. */\n response?: Response\n\n /**\n * @param message - The error message.\n * @param init - The extra fields to attach to the error.\n */\n constructor (message: string, init: FetchjaErrorInit = {}) {\n super(message)\n\n this.name = 'FetchjaError'\n this.status = init.status\n this.statusText = init.statusText\n this.errors = init.errors\n this.response = init.response\n }\n}\n","import { FetchjaError } from './errors.js'\n\n/**\n * Property names that could pollute an object's prototype. They are\n * skipped while reading the input object.\n */\nconst DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n/**\n * The transforms applied to resource `type` names while serializing.\n */\nexport interface SerializeOptions {\n /** Cases a `type` name. */\n caseType: (type: string) => string\n\n /** Pluralizes a `type` name. */\n pluralTypes: (type: string) => string\n}\n\n/**\n * Check whether a value is a plain object. Arrays, `null`, and `Date`\n * instances are treated as values, not relationships.\n *\n * @param value - The value to check.\n * @returns `true` when the value is a plain object.\n */\nfunction isPlainObject (\n value: unknown\n): value is Record<string, unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n )\n}\n\n/**\n * Serialize a plain object into a JSON:API request document. Nested\n * objects with an `id` become relationships, and the full resources are\n * collected into the top-level `included` array.\n *\n * @param type - The resource type of the root object.\n * @param input - The plain object to serialize.\n * @param options - The type-name transforms.\n * @returns The JSON:API document as a JSON string.\n */\nexport function serialize (\n type: string,\n input: Record<string, unknown>,\n options: SerializeOptions\n): string {\n const included: Record<string, unknown>[] = []\n const includedKeys = new Set<string>()\n\n /**\n * Apply the configured type-name transforms.\n *\n * @param rawType - The raw type name.\n * @returns The transformed type name.\n */\n function formatType (rawType: string): string {\n return options.pluralTypes(options.caseType(rawType))\n }\n\n /**\n * Resolve a resource's `type`: its own `type` when present, otherwise\n * one derived from the relationship key. Used for both the identifier\n * and the included resource, so the two always match.\n *\n * @param node - The resource.\n * @param fallbackType - The relationship key to fall back to.\n * @returns The resource type.\n */\n function resourceType (\n node: Record<string, unknown>,\n fallbackType: string\n ): string {\n return (node.type as string) ?? formatType(fallbackType)\n }\n\n /**\n * Build a JSON:API resource identifier (`{ type, id }`).\n *\n * @param resource - The related resource.\n * @param fallbackType - The type to use when the resource has none.\n * @returns The resource identifier.\n */\n function toIdentifier (\n resource: Record<string, unknown>,\n fallbackType: string\n ): Record<string, unknown> {\n return {\n type: resourceType(resource, fallbackType),\n id: String(resource.id)\n }\n }\n\n /**\n * Collect a related resource into `included`, de-duplicated by its\n * `type` and `id`.\n *\n * @param resource - The related resource.\n * @param fallbackType - The type to use when the resource has none.\n */\n function collectIncluded (\n resource: unknown,\n fallbackType: string\n ): void {\n if (!isPlainObject(resource)) {\n return\n }\n\n if (resource.id == null) {\n throw new FetchjaError('All included resources must have an ID.')\n }\n\n const key = `${resource.type ?? fallbackType}:${resource.id}`\n\n if (includedKeys.has(key)) {\n return\n }\n\n includedKeys.add(key)\n included.push(extractResource(resource, fallbackType))\n }\n\n /**\n * Build a JSON:API resource object from a plain object, splitting its\n * fields into `attributes` and `relationships`.\n *\n * @param node - The plain object to convert.\n * @param rawType - The raw type name of the resource.\n * @returns The JSON:API resource object.\n */\n function extractResource (\n node: Record<string, unknown>,\n rawType: string\n ): Record<string, unknown> {\n const data: Record<string, unknown> = {\n type: resourceType(node, rawType)\n }\n const relationships: Record<string, unknown> = {}\n const attributes: Record<string, unknown> = {}\n\n for (const key in node) {\n if (DANGEROUS_KEYS.has(key) || key === 'type') {\n continue\n }\n\n if (key === 'id') {\n data.id = String(node.id)\n\n continue\n }\n\n const value = node[key]\n\n if (Array.isArray(value)) {\n relationships[key] = {\n data: value.map(item => {\n collectIncluded(item, key)\n\n return toIdentifier(item, key)\n })\n }\n\n continue\n }\n\n if (isPlainObject(value)) {\n relationships[key] = { data: toIdentifier(value, key) }\n collectIncluded(value, key)\n\n continue\n }\n\n attributes[key] = value\n }\n\n if (Object.keys(attributes).length > 0) {\n data.attributes = attributes\n }\n\n if (Object.keys(relationships).length > 0) {\n data.relationships = relationships\n }\n\n return data\n }\n\n const data = extractResource(input, type)\n\n return JSON.stringify({ data, included })\n}\n","/**\n * Property names that could pollute an object's prototype. They are\n * skipped while reading the parameters object.\n */\nconst DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n/**\n * Check whether a value should be walked into when building the query.\n * Arrays and plain objects are traversable; `Date` instances are not.\n *\n * @param value - The value to check.\n * @returns `true` when the value should be traversed.\n */\nfunction isTraversable (value: unknown): value is object {\n return (\n typeof value === 'object' &&\n value !== null &&\n !(value instanceof Date)\n )\n}\n\n/**\n * Check whether a value is an array made up entirely of scalar items.\n * JSON:API serializes list members such as `include`, `sort`, `fields`\n * and array filters as a single comma-separated value, so these arrays\n * are joined rather than expanded into bracketed keys. Arrays that hold\n * objects (e.g. boolean filter groups) fall back to bracket recursion.\n *\n * @param value - The value to check.\n * @returns `true` when the value is an array of scalars.\n */\nfunction isScalarArray (value: unknown): value is unknown[] {\n return Array.isArray(value) && !value.some(isTraversable)\n}\n\n/**\n * Append an object's entries to a query, recursing into nested objects\n * and arrays using JSON:API-friendly bracket notation.\n *\n * @param query - The query to append to.\n * @param object - The object or array to walk.\n * @param prefix - The key prefix built from parent keys.\n */\nfunction buildQuery (\n query: URLSearchParams,\n object: object,\n prefix = ''\n): void {\n const isArray = Array.isArray(object)\n\n for (const key in object) {\n if (DANGEROUS_KEYS.has(key)) {\n continue\n }\n\n const value = (object as Record<string, unknown>)[key]\n const path = prefix ? `${prefix}[${isArray ? '' : key}]` : key\n\n if (isScalarArray(value) && value.length > 0) {\n query.append(path, value.map(String).join(','))\n } else if (isTraversable(value)) {\n buildQuery(query, value, path)\n } else {\n query.append(path, String(value))\n }\n }\n}\n\n/**\n * Serialize a plain object into URL query parameters.\n *\n * @param parameters - The parameters to serialize.\n * @returns The serialized query parameters.\n */\nexport function queryFormatter (\n parameters: Record<string, unknown> = {}\n): URLSearchParams {\n const query = new URLSearchParams()\n\n buildQuery(query, parameters)\n\n return query\n}\n","/**\n * The transforms used to turn a model name into a URL path segment.\n */\nexport interface ModelOptions {\n /** Cases a single path segment. */\n resourceCase: (segment: string) => string\n\n /** Pluralizes a single path segment. */\n pluralize: (segment: string) => string\n}\n\n/**\n * Check whether a path segment is a numeric resource id.\n *\n * @param segment - The path segment to check.\n * @returns `true` when the segment is all digits.\n */\nfunction isResourceId (segment: string): boolean {\n return /^\\d+$/.test(segment)\n}\n\n/**\n * Normalize a `get` model into a URL path. Only the resource being\n * addressed (the last non-numeric segment) is cased and pluralized, so\n * namespaces and numeric ids are left untouched. This keeps `get`\n * consistent with the path built by {@link splitModel}.\n *\n * @param model - The model path, e.g. `article`, `articles/1`.\n * @param options - The casing and pluralization transforms.\n * @returns The normalized URL path.\n */\nexport function normalizePath (\n model: string,\n options: ModelOptions\n): string {\n const segments = model.split('/').filter(Boolean)\n\n const targetIndex = segments.reduce(\n (last, segment, index) => (isResourceId(segment) ? last : index),\n -1\n )\n\n return segments\n .map((segment, index) =>\n index === targetIndex\n ? options.pluralize(options.resourceCase(segment))\n : segment\n )\n .join('/')\n}\n\n/**\n * Split a write model into its resource `type` and URL path. The last\n * segment is treated as the resource and is cased and pluralized.\n *\n * @param url - The model path, e.g. `article`, `admin/article`.\n * @param options - The casing and pluralization transforms.\n * @returns A `[type, path]` tuple.\n */\nexport function splitModel (\n url: string,\n options: ModelOptions\n): [string, string] {\n const parts = url.split('/').filter(Boolean)\n const model = parts.pop() ?? ''\n const namespace = parts.join('/')\n const path = options.pluralize(options.resourceCase(model))\n\n return [model, namespace ? `${namespace}/${path}` : path]\n}\n","/**\n * Convert a string to `camelCase` from `snake_case`, `kebab-case`, or\n * `SCREAMING_SNAKE_CASE`.\n *\n * @param input - The string to convert.\n * @returns The converted string.\n */\nexport function camelCase (input: string): string {\n return input\n .toLowerCase()\n .replace(/[-_](.)/g, (_match, character: string) =>\n character.toUpperCase()\n )\n .replace(/^(.)/, character => character.toLowerCase())\n}\n\n/**\n * Convert a string to `kebab-case` from `camelCase` or `snake_case`.\n *\n * @param input - The string to convert.\n * @returns The converted string.\n */\nexport function kebabCase (input: string): string {\n return input\n .replace(/([a-z])([A-Z])/g, '$1-$2')\n .replace(/_/g, '-')\n .toLowerCase()\n}\n\n/**\n * Convert a string to `snake_case` from `camelCase` or `kebab-case`.\n *\n * @param input - The string to convert.\n * @returns The converted string.\n */\nexport function snakeCase (input: string): string {\n return input\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n .replace(/-/g, '_')\n .toLowerCase()\n}\n","/**\n * Pluralize an English word using a small set of common rules.\n *\n * This is a lightweight, dependency-free helper that covers the cases\n * most JSON:API resource names need. It is intentionally simple and\n * idempotent: a word that already looks plural is returned unchanged.\n * For irregular words (such as `person` becoming `people`), inject a\n * fuller implementation through the `pluralize` option.\n *\n * @param word - The word to pluralize.\n * @returns The pluralized word.\n */\nexport function pluralize (word: string): string {\n if (word === '') {\n return word\n }\n\n // Singular words ending in \"us\" take \"es\": status -> statuses,\n // bus -> buses, virus -> viruses.\n if (/us$/i.test(word)) {\n return `${word}es`\n }\n\n // Anything else ending in \"s\" is treated as already plural.\n if (/s$/i.test(word)) {\n return word\n }\n\n // A consonant followed by \"y\" becomes \"ies\": category -> categories.\n if (/[^aeiou]y$/i.test(word)) {\n return word.replace(/y$/i, 'ies')\n }\n\n // Sibilant endings take \"es\": box -> boxes, match -> matches.\n if (/(x|z|ch|sh)$/i.test(word)) {\n return `${word}es`\n }\n\n return `${word}s`\n}\n","import { deserialize } from './deserialize.js'\nimport { serialize } from './serialize.js'\nimport { queryFormatter } from './query.js'\nimport { normalizePath, splitModel, type ModelOptions } from './model.js'\nimport { camelCase, kebabCase, snakeCase } from './case.js'\nimport { pluralize as defaultPluralize } from './pluralize.js'\nimport { FetchjaError } from './errors.js'\nimport type { FetchjaOptions, RequestOptions } from './types.js'\n\n/** The media type required by the JSON:API specification. */\nconst JSON_API_MEDIA_TYPE = 'application/vnd.api+json'\n\n/**\n * Return the given string unchanged. Used as the default transform when\n * no resource casing or pluralization is requested.\n *\n * @param value - The string to return.\n * @returns The same string.\n */\nfunction identity (value: string): string {\n return value\n}\n\n/**\n * The resource-casing strategies selectable through `resourceCase`.\n * Stored on a null-prototype object so an unexpected option value cannot\n * reach inherited properties.\n */\nconst RESOURCE_CASES: Record<string, (value: string) => string> =\n Object.assign(Object.create(null), {\n camel: camelCase,\n kebab: kebabCase,\n snake: snakeCase,\n none: identity\n })\n\n/**\n * The default query serializer, used when no `queryFormatter` option is\n * provided.\n *\n * @param params - The query parameters to serialize.\n * @returns The serialized query parameters.\n */\nfunction defaultQueryFormatter (\n params: unknown\n): string | URLSearchParams {\n return queryFormatter(params as Record<string, unknown>)\n}\n\n/**\n * Resolve the `pluralize` option into a transform function.\n *\n * @param option - The `pluralize` option value.\n * @returns The pluralization transform.\n */\nfunction resolvePluralizer (\n option: FetchjaOptions['pluralize']\n): (value: string) => string {\n if (option === false) {\n return identity\n }\n\n if (typeof option === 'function') {\n return option\n }\n\n return defaultPluralize\n}\n\n/**\n * Copy every response header into a plain object.\n *\n * @param response - The response to read headers from.\n * @returns A plain object of header names to values.\n */\nfunction collectHeaders (response: Response): Record<string, string> {\n const headers: Record<string, string> = {}\n\n for (const [key, value] of response.headers.entries()) {\n headers[key] = value\n }\n\n return headers\n}\n\n/**\n * A super simple, lightweight JSON:API client built on the Fetch API.\n */\nexport default class Fetchja {\n /** The base URL prepended to every request. */\n baseURL?: string\n\n /** Headers merged into every request. */\n headers: Record<string, string>\n\n /** Serializes the request query parameters. */\n queryFormatter: (params: unknown) => string | URLSearchParams\n\n /** Cases `type` names when serializing request bodies. */\n typeCase: (value: string) => string\n\n /** Cases resource names in the URL path. */\n resourceCase: (value: string) => string\n\n /** Pluralizes resource names. */\n pluralize: (value: string) => string\n\n /** Interceptor invoked on a non-OK response, before throwing. */\n onResponseError: FetchjaOptions['onResponseError']\n\n /** The custom fetch implementation, when provided. */\n readonly #fetch?: typeof fetch\n\n /** Alias of {@link Fetchja.get}. */\n fetch!: Fetchja['get']\n\n /** Alias of {@link Fetchja.post}. */\n create!: Fetchja['post']\n\n /** Alias of {@link Fetchja.patch}. */\n update!: Fetchja['patch']\n\n /** Alias of {@link Fetchja.delete}. */\n remove!: Fetchja['delete']\n\n /**\n * @param options - The client options.\n */\n constructor (options: FetchjaOptions = {}) {\n this.baseURL = options.baseURL\n\n this.headers = {\n 'Accept': JSON_API_MEDIA_TYPE,\n 'Content-Type': JSON_API_MEDIA_TYPE,\n ...options.headers\n }\n\n this.#fetch = options.fetch\n\n this.queryFormatter = typeof options.queryFormatter === 'function'\n ? options.queryFormatter\n : defaultQueryFormatter\n\n this.resourceCase =\n RESOURCE_CASES[options.resourceCase ?? 'none'] ?? identity\n\n this.typeCase =\n RESOURCE_CASES[options.typeCase ?? 'camel'] ?? camelCase\n\n this.pluralize = resolvePluralizer(options.pluralize)\n\n this.onResponseError = options.onResponseError\n\n this.fetch = this.get\n this.create = this.post\n this.update = this.patch\n this.remove = this.delete\n }\n\n /** The transforms shared by the path-building helpers. */\n get #modelOptions (): ModelOptions {\n return {\n resourceCase: this.resourceCase,\n pluralize: this.pluralize\n }\n }\n\n /**\n * Perform a request and return the deserialized response. Throws a\n * {@link FetchjaError} on a non-OK response.\n *\n * @param options - The request options.\n * @returns The deserialized response.\n */\n async request (\n options: RequestOptions\n ): Promise<Record<string, unknown>> {\n const base = this.baseURL ?? options.baseURL\n\n if (base === undefined || base === '') {\n throw new FetchjaError(\n 'A `baseURL` is required to make requests.'\n )\n }\n\n const requestPath = options.url ?? ''\n\n const relativePath = requestPath.startsWith('/')\n ? requestPath.slice(1)\n : requestPath\n\n const baseWithSlash = base.endsWith('/') ? base : `${base}/`\n const url = new URL(relativePath, baseWithSlash)\n\n if (options.params !== undefined) {\n url.search = String(this.queryFormatter(options.params))\n }\n\n const body = options.body !== undefined && options.type !== undefined\n ? serialize(options.type, options.body, {\n caseType: this.typeCase,\n pluralTypes: this.pluralize\n })\n : undefined\n\n const requestFetch = this.#fetch ?? fetch\n const baseHeaders = this.headers\n\n /**\n * Send the request. Defined as a closure so it can be replayed by\n * the `onResponseError` interceptor with the current headers.\n *\n * @returns The raw response.\n */\n function sendRequest (): Promise<Response> {\n const headers = new Headers({\n ...baseHeaders,\n ...options.headers\n })\n\n const init: RequestInit = {\n method: options.method,\n headers,\n body\n }\n\n return requestFetch(url, init)\n }\n\n const initialResponse = await sendRequest()\n\n const augmentedResponse = Object.assign(initialResponse, {\n replayRequest: sendRequest\n })\n\n const handledResponse =\n augmentedResponse.ok || this.onResponseError === undefined\n ? augmentedResponse\n : await this.onResponseError(augmentedResponse)\n\n const response = handledResponse instanceof Response\n ? handledResponse\n : augmentedResponse\n\n const responseHeaders = collectHeaders(response)\n const contentType = responseHeaders['content-type'] ?? ''\n\n const payload = contentType.includes(JSON_API_MEDIA_TYPE)\n ? await response.json()\n : {}\n\n if (!response.ok) {\n throw new FetchjaError(response.statusText || 'Request failed', {\n status: response.status,\n statusText: response.statusText,\n errors: payload.errors,\n response\n })\n }\n\n return {\n ...deserialize(payload),\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders\n }\n }\n\n /**\n * Read one or more resources.\n *\n * @param model - The resource path, e.g. `articles` or `articles/1`.\n * @param options - Extra request options.\n * @returns The deserialized response.\n */\n get (\n model: string,\n options: RequestOptions = {}\n ): Promise<Record<string, unknown>> {\n return this.request({\n ...options,\n method: options.method ?? 'GET',\n url: normalizePath(model, this.#modelOptions)\n })\n }\n\n /**\n * Create a resource.\n *\n * @param model - The resource name, e.g. `article`.\n * @param body - The resource to create.\n * @param options - Extra request options.\n * @returns The deserialized response.\n */\n post (\n model: string,\n body: Record<string, unknown>,\n options: RequestOptions = {}\n ): Promise<Record<string, unknown>> {\n const [type, url] = splitModel(model, this.#modelOptions)\n\n return this.request({\n ...options,\n method: options.method ?? 'POST',\n url,\n body,\n type\n })\n }\n\n /**\n * Update a resource. When the body has an `id`, it is appended to the\n * URL.\n *\n * @param model - The resource name, e.g. `article`.\n * @param body - The fields to update.\n * @param options - Extra request options.\n * @returns The deserialized response.\n */\n patch (\n model: string,\n body: Record<string, unknown>,\n options: RequestOptions = {}\n ): Promise<Record<string, unknown>> {\n const [type, url] = splitModel(model, this.#modelOptions)\n\n const resourceUrl = body.id !== undefined\n ? `${url}/${String(body.id)}`\n : url\n\n return this.request({\n ...options,\n method: options.method ?? 'PATCH',\n url: resourceUrl,\n body,\n type\n })\n }\n\n /**\n * Delete a resource. No request body is sent.\n *\n * @param model - The resource name, e.g. `article`.\n * @param id - The id of the resource to delete.\n * @param options - Extra request options.\n * @returns The deserialized response.\n */\n delete (\n model: string,\n id: string,\n options: RequestOptions = {}\n ): Promise<Record<string, unknown>> {\n const [, url] = splitModel(model, this.#modelOptions)\n\n return this.request({\n ...options,\n method: options.method ?? 'DELETE',\n url: `${url}/${id}`\n })\n }\n}\n"],"mappings":"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","names":["DANGEROUS_KEYS","deattribute","data","item","output","key","relation","isObject","value","deserialize","response","output","deattribute","resourcesByKey","resource","flat","resolve","reference","replace","link","entry","key","data","FetchjaError","message","init","DANGEROUS_KEYS","isPlainObject","value","serialize","type","input","options","included","includedKeys","formatType","rawType","resourceType","node","fallbackType","toIdentifier","resource","collectIncluded","FetchjaError","key","extractResource","data","relationships","attributes","item","DANGEROUS_KEYS","isTraversable","value","isScalarArray","buildQuery","query","object","prefix","isArray","key","path","queryFormatter","parameters","isResourceId","segment","normalizePath","model","options","segments","targetIndex","last","index","splitModel","url","parts","namespace","path","camelCase","input","_match","character","kebabCase","snakeCase","pluralize","word","JSON_API_MEDIA_TYPE","identity","value","RESOURCE_CASES","camelCase","kebabCase","snakeCase","defaultQueryFormatter","params","queryFormatter","resolvePluralizer","option","pluralize","collectHeaders","response","headers","key","Fetchja","#fetch","options","#modelOptions","base","FetchjaError","requestPath","relativePath","baseWithSlash","url","body","serialize","requestFetch","baseHeaders","sendRequest","init","initialResponse","augmentedResponse","handledResponse","responseHeaders","payload","deserialize","model","normalizePath","type","splitModel","resourceUrl","id"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fetchja",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "A super simple, modern, lightweight, zero-dependency JSON:API client built on the Fetch API.",
6
6
  "author": "Caio Tarifa <caio@yahoo.com>",
package/readme.md CHANGED
@@ -153,15 +153,15 @@ console.log(data.author.name) // comes from `included`
153
153
 
154
154
  ## Query parameters
155
155
 
156
- Pass a `params` object. Fetchja turns nested objects and arrays into JSON:API-style query strings:
156
+ Pass a `params` object. Fetchja serializes it into [JSON:API 1.1](https://jsonapi.org/format/1.1/#query-parameters) query strings: nested objects become bracketed keys, and arrays become comma-separated values (the format the spec defines for `include`, `sort`, `fields[type]`, and list filters).
157
157
 
158
158
  ```js
159
159
  const { data, meta } = await api.get('articles', {
160
160
  params: {
161
161
  include: ['author', 'comments'],
162
- fields: { articles: 'title,body' },
162
+ fields: { articles: ['title', 'body'] },
163
163
  filter: { published: true },
164
- sort: '-createdAt',
164
+ sort: ['-createdAt'],
165
165
  page: { number: 1, size: 10 }
166
166
  }
167
167
  })
@@ -170,26 +170,45 @@ const { data, meta } = await api.get('articles', {
170
170
  This becomes:
171
171
 
172
172
  ```
173
- /articles?include[]=author&include[]=comments&fields[articles]=title,body&filter[published]=true&sort=-createdAt&page[number]=1&page[size]=10
173
+ /articles?include=author,comments&fields[articles]=title,body&filter[published]=true&sort=-createdAt&page[number]=1&page[size]=10
174
+ ```
175
+
176
+ Strings work too, so you can pass the comma-separated form directly if you prefer — `include: 'author,comments'` and `sort: '-createdAt'` produce the same output.
177
+
178
+ Filters of any depth are supported. Object values nest with brackets, scalar arrays join with commas, and arrays of objects (e.g. boolean groups) expand into indexed keys:
179
+
180
+ ```js
181
+ await api.get('articles', {
182
+ params: {
183
+ filter: {
184
+ id: [1, 2, 3], // filter[id]=1,2,3
185
+ price: { gte: 10, lte: 100 }, // filter[price][gte]=10&filter[price][lte]=100
186
+ tags: { any: ['news', 'tech'] }, // filter[tags][any]=news,tech
187
+ or: [{ status: 'active' }, { status: 'pending' }]
188
+ // filter[or][][status]=active&filter[or][][status]=pending
189
+ }
190
+ }
191
+ })
174
192
  ```
175
193
 
176
194
  ## Make your own query formatter
177
195
 
178
- Some servers want a different format. For example, they may want `include=author,comments` (one key, values joined by commas) instead of `include[]=...`. You can pass your own function:
196
+ The default formatter is JSON:API 1.1 compliant. Some servers want a different shape for example, repeated `include[]=author&include[]=comments` keys instead of one comma-separated value. You can pass your own function:
179
197
 
180
198
  ```js
181
199
  import Fetchja from 'fetchja'
182
200
 
183
- function commaQueryFormatter (params) {
201
+ function bracketQueryFormatter (params) {
184
202
  const search = new URLSearchParams()
185
203
 
186
204
  for (const key in params) {
187
205
  const value = params[key]
188
206
 
189
- search.append(
190
- key,
191
- Array.isArray(value) ? value.join(',') : String(value)
192
- )
207
+ if (Array.isArray(value)) {
208
+ for (const item of value) search.append(`${key}[]`, String(item))
209
+ } else {
210
+ search.append(key, String(value))
211
+ }
193
212
  }
194
213
 
195
214
  return search
@@ -197,11 +216,11 @@ function commaQueryFormatter (params) {
197
216
 
198
217
  const api = new Fetchja({
199
218
  baseURL: 'https://api.example.com',
200
- queryFormatter: commaQueryFormatter
219
+ queryFormatter: bracketQueryFormatter
201
220
  })
202
221
 
203
222
  await api.get('articles', { params: { include: ['author', 'comments'] } })
204
- // -> /articles?include=author,comments
223
+ // -> /articles?include[]=author&include[]=comments
205
224
  ```
206
225
 
207
226
  Your function takes the `params` object and returns a string or a `URLSearchParams`.