fetchja 1.4.0 → 2.0.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.d.ts +217 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -7
- package/package.json +30 -10
- package/readme.md +306 -18
- package/src/index.js +0 -243
- package/src/utils/camel-case.js +0 -12
- package/src/utils/deattribute.js +0 -28
- package/src/utils/deserialize.js +0 -93
- package/src/utils/error-parser.js +0 -17
- package/src/utils/kebab-case.js +0 -12
- package/src/utils/query-formatter.js +0 -34
- package/src/utils/serialize.js +0 -98
- package/src/utils/snake-case.js +0 -12
- package/src/utils/split-model.js +0 -20
- package/tests/utils/camel-case.test.js +0 -30
- package/tests/utils/kebab-case.test.js +0 -30
- package/tests/utils/snake-case.test.js +0 -30
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The options accepted by the {@link Fetchja} constructor.
|
|
3
|
+
*/
|
|
4
|
+
interface FetchjaOptions {
|
|
5
|
+
/** The base URL prepended to every request. */
|
|
6
|
+
baseURL?: string;
|
|
7
|
+
/** Headers merged into every request. */
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
/** A custom fetch implementation. Defaults to the global `fetch`. */
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
/** A custom serializer for the request query parameters. */
|
|
12
|
+
queryFormatter?: (params: unknown) => string | URLSearchParams;
|
|
13
|
+
/** How resource names in the URL path are cased. */
|
|
14
|
+
resourceCase?: 'camel' | 'kebab' | 'snake' | 'none';
|
|
15
|
+
/** How `type` names are cased when serializing request bodies. */
|
|
16
|
+
typeCase?: 'camel' | 'kebab' | 'snake' | 'none';
|
|
17
|
+
/**
|
|
18
|
+
* Pluralize resource names. `true` uses the built-in pluralizer,
|
|
19
|
+
* `false` disables it, and a function injects a custom one.
|
|
20
|
+
*/
|
|
21
|
+
pluralize?: boolean | ((word: string) => string);
|
|
22
|
+
/**
|
|
23
|
+
* Interceptor invoked on a non-OK response, before Fetchja throws.
|
|
24
|
+
* The response is augmented with `replayRequest` so the original
|
|
25
|
+
* request can be retried. Return a `Response` to continue with it.
|
|
26
|
+
*/
|
|
27
|
+
onResponseError?: (response: Response & {
|
|
28
|
+
replayRequest: () => Promise<Response>;
|
|
29
|
+
}) => Response | void | Promise<Response | void>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The options accepted by {@link Fetchja.request} and the request
|
|
33
|
+
* methods. Most callers only set `params`, `headers`, or `method`.
|
|
34
|
+
*/
|
|
35
|
+
interface RequestOptions {
|
|
36
|
+
/** The request path, relative to the base URL. */
|
|
37
|
+
url?: string;
|
|
38
|
+
/** A base URL for this request only. */
|
|
39
|
+
baseURL?: string;
|
|
40
|
+
/** The HTTP method. */
|
|
41
|
+
method?: string;
|
|
42
|
+
/** Headers for this request only. */
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
/** The query parameters to serialize. */
|
|
45
|
+
params?: unknown;
|
|
46
|
+
/** The request body to serialize. */
|
|
47
|
+
body?: Record<string, unknown>;
|
|
48
|
+
/** The resource type used to serialize the body. */
|
|
49
|
+
type?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A super simple, lightweight JSON:API client built on the Fetch API.
|
|
54
|
+
*/
|
|
55
|
+
declare class Fetchja {
|
|
56
|
+
#private;
|
|
57
|
+
/** The base URL prepended to every request. */
|
|
58
|
+
baseURL?: string;
|
|
59
|
+
/** Headers merged into every request. */
|
|
60
|
+
headers: Record<string, string>;
|
|
61
|
+
/** Serializes the request query parameters. */
|
|
62
|
+
queryFormatter: (params: unknown) => string | URLSearchParams;
|
|
63
|
+
/** Cases `type` names when serializing request bodies. */
|
|
64
|
+
typeCase: (value: string) => string;
|
|
65
|
+
/** Cases resource names in the URL path. */
|
|
66
|
+
resourceCase: (value: string) => string;
|
|
67
|
+
/** Pluralizes resource names. */
|
|
68
|
+
pluralize: (value: string) => string;
|
|
69
|
+
/** Interceptor invoked on a non-OK response, before throwing. */
|
|
70
|
+
onResponseError: FetchjaOptions['onResponseError'];
|
|
71
|
+
/** Alias of {@link Fetchja.get}. */
|
|
72
|
+
fetch: Fetchja['get'];
|
|
73
|
+
/** Alias of {@link Fetchja.post}. */
|
|
74
|
+
create: Fetchja['post'];
|
|
75
|
+
/** Alias of {@link Fetchja.patch}. */
|
|
76
|
+
update: Fetchja['patch'];
|
|
77
|
+
/** Alias of {@link Fetchja.delete}. */
|
|
78
|
+
remove: Fetchja['delete'];
|
|
79
|
+
/**
|
|
80
|
+
* @param options - The client options.
|
|
81
|
+
*/
|
|
82
|
+
constructor(options?: FetchjaOptions);
|
|
83
|
+
/**
|
|
84
|
+
* Perform a request and return the deserialized response. Throws a
|
|
85
|
+
* {@link FetchjaError} on a non-OK response.
|
|
86
|
+
*
|
|
87
|
+
* @param options - The request options.
|
|
88
|
+
* @returns The deserialized response.
|
|
89
|
+
*/
|
|
90
|
+
request(options: RequestOptions): Promise<Record<string, unknown>>;
|
|
91
|
+
/**
|
|
92
|
+
* Read one or more resources.
|
|
93
|
+
*
|
|
94
|
+
* @param model - The resource path, e.g. `articles` or `articles/1`.
|
|
95
|
+
* @param options - Extra request options.
|
|
96
|
+
* @returns The deserialized response.
|
|
97
|
+
*/
|
|
98
|
+
get(model: string, options?: RequestOptions): Promise<Record<string, unknown>>;
|
|
99
|
+
/**
|
|
100
|
+
* Create a resource.
|
|
101
|
+
*
|
|
102
|
+
* @param model - The resource name, e.g. `article`.
|
|
103
|
+
* @param body - The resource to create.
|
|
104
|
+
* @param options - Extra request options.
|
|
105
|
+
* @returns The deserialized response.
|
|
106
|
+
*/
|
|
107
|
+
post(model: string, body: Record<string, unknown>, options?: RequestOptions): Promise<Record<string, unknown>>;
|
|
108
|
+
/**
|
|
109
|
+
* Update a resource. When the body has an `id`, it is appended to the
|
|
110
|
+
* URL.
|
|
111
|
+
*
|
|
112
|
+
* @param model - The resource name, e.g. `article`.
|
|
113
|
+
* @param body - The fields to update.
|
|
114
|
+
* @param options - Extra request options.
|
|
115
|
+
* @returns The deserialized response.
|
|
116
|
+
*/
|
|
117
|
+
patch(model: string, body: Record<string, unknown>, options?: RequestOptions): Promise<Record<string, unknown>>;
|
|
118
|
+
/**
|
|
119
|
+
* Delete a resource. No request body is sent.
|
|
120
|
+
*
|
|
121
|
+
* @param model - The resource name, e.g. `article`.
|
|
122
|
+
* @param id - The id of the resource to delete.
|
|
123
|
+
* @param options - Extra request options.
|
|
124
|
+
* @returns The deserialized response.
|
|
125
|
+
*/
|
|
126
|
+
delete(model: string, id: string, options?: RequestOptions): Promise<Record<string, unknown>>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A single error object as defined by the JSON:API specification.
|
|
131
|
+
*
|
|
132
|
+
* @see https://jsonapi.org/format/#error-objects
|
|
133
|
+
*/
|
|
134
|
+
interface JsonApiError {
|
|
135
|
+
/** The HTTP status code, as a string. */
|
|
136
|
+
status?: string;
|
|
137
|
+
/** An application-specific error code. */
|
|
138
|
+
code?: string;
|
|
139
|
+
/** A short, human-readable summary of the problem. */
|
|
140
|
+
title?: string;
|
|
141
|
+
/** A human-readable explanation of this occurrence of the problem. */
|
|
142
|
+
detail?: string;
|
|
143
|
+
/** References to the source of the error. */
|
|
144
|
+
source?: Record<string, unknown>;
|
|
145
|
+
[key: string]: unknown;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The extra fields used to build a {@link FetchjaError}.
|
|
149
|
+
*/
|
|
150
|
+
interface FetchjaErrorInit {
|
|
151
|
+
/** The HTTP status code of the failed response. */
|
|
152
|
+
status?: number;
|
|
153
|
+
/** The HTTP status text of the failed response. */
|
|
154
|
+
statusText?: string;
|
|
155
|
+
/** The JSON:API error objects returned by the server. */
|
|
156
|
+
errors?: JsonApiError[];
|
|
157
|
+
/** The raw failed response. */
|
|
158
|
+
response?: Response;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The error thrown when a request fails. It carries the HTTP status and
|
|
162
|
+
* any JSON:API error objects returned by the server.
|
|
163
|
+
*/
|
|
164
|
+
declare class FetchjaError extends Error {
|
|
165
|
+
/** The HTTP status code of the failed response. */
|
|
166
|
+
status?: number;
|
|
167
|
+
/** The HTTP status text of the failed response. */
|
|
168
|
+
statusText?: string;
|
|
169
|
+
/** The JSON:API error objects returned by the server. */
|
|
170
|
+
errors?: JsonApiError[];
|
|
171
|
+
/** The raw failed response. */
|
|
172
|
+
response?: Response;
|
|
173
|
+
/**
|
|
174
|
+
* @param message - The error message.
|
|
175
|
+
* @param init - The extra fields to attach to the error.
|
|
176
|
+
*/
|
|
177
|
+
constructor(message: string, init?: FetchjaErrorInit);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Convert a string to `camelCase` from `snake_case`, `kebab-case`, or
|
|
182
|
+
* `SCREAMING_SNAKE_CASE`.
|
|
183
|
+
*
|
|
184
|
+
* @param input - The string to convert.
|
|
185
|
+
* @returns The converted string.
|
|
186
|
+
*/
|
|
187
|
+
declare function camelCase(input: string): string;
|
|
188
|
+
/**
|
|
189
|
+
* Convert a string to `kebab-case` from `camelCase` or `snake_case`.
|
|
190
|
+
*
|
|
191
|
+
* @param input - The string to convert.
|
|
192
|
+
* @returns The converted string.
|
|
193
|
+
*/
|
|
194
|
+
declare function kebabCase(input: string): string;
|
|
195
|
+
/**
|
|
196
|
+
* Convert a string to `snake_case` from `camelCase` or `kebab-case`.
|
|
197
|
+
*
|
|
198
|
+
* @param input - The string to convert.
|
|
199
|
+
* @returns The converted string.
|
|
200
|
+
*/
|
|
201
|
+
declare function snakeCase(input: string): string;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Pluralize an English word using a small set of common rules.
|
|
205
|
+
*
|
|
206
|
+
* This is a lightweight, dependency-free helper that covers the cases
|
|
207
|
+
* most JSON:API resource names need. It is intentionally simple and
|
|
208
|
+
* idempotent: a word that already looks plural is returned unchanged.
|
|
209
|
+
* For irregular words (such as `person` becoming `people`), inject a
|
|
210
|
+
* fuller implementation through the `pluralize` option.
|
|
211
|
+
*
|
|
212
|
+
* @param word - The word to pluralize.
|
|
213
|
+
* @returns The pluralized word.
|
|
214
|
+
*/
|
|
215
|
+
declare function pluralize(word: string): string;
|
|
216
|
+
|
|
217
|
+
export { FetchjaError, type FetchjaErrorInit, type FetchjaOptions, type JsonApiError, type RequestOptions, camelCase, Fetchja as default, kebabCase, pluralize, snakeCase };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
//# sourceMappingURL=index.js.map
|
|
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};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/index.js", "../src/utils/deattribute.js", "../src/utils/deserialize.js", "../src/utils/error-parser.js", "../src/utils/serialize.js", "../src/utils/query-formatter.js", "../src/utils/split-model.js", "../src/utils/camel-case.js", "../src/utils/kebab-case.js", "../src/utils/snake-case.js"],
|
|
4
|
-
"sourcesContent": ["import pluralize from 'pluralize'\n\nimport { deserialize } from './utils/deserialize.js'\nimport { serialize } from './utils/serialize.js'\n\nimport { errorParser } from './utils/error-parser.js'\nimport { queryFormatter } from './utils/query-formatter.js'\nimport { splitModel } from './utils/split-model.js'\n\nimport { camelCase } from './utils/camel-case.js'\nimport { kebabCase } from './utils/kebab-case.js'\nimport { snakeCase } from './utils/snake-case.js'\n\nconst jsonType = 'application/vnd.api+json'\n\n/**\n * Options for Fetchja.\n * \n * @typedef {Object} FetchjaOptions\n * @property {string} baseURL The base URL for all requests.\n * @property {Function} fetchFunction A custom fetch function to use in request.\n * @property {Object} headers The headers to include in all requests.\n * @property {Function} queryFormatter A function to format query parameters.\n * @property {string} resourceCase The case to use for resource names.\n * @property {boolean} pluralize Pluralize resource names.\n */\n\n/**\n * Fetchja is a simple wrapper around the Fetch API.\n * \n * @class Fetchja\n * @param {FetchjaOptions} [options] Options for Fetchja.\n */\nexport default class Fetchja {\n constructor (options = {\n headers: {}\n }) {\n this.baseURL = options.baseURL\n\n // Headers\n this.headers = {\n Accept: jsonType,\n 'Content-Type': jsonType,\n ...options.headers\n }\n\n // Fetch Function\n this.fetchFunction = options.fetchFunction\n\n // Query\n this.queryFormatter = typeof options.queryFormatter === 'function'\n ? options.queryFormatter\n : object => queryFormatter(object)\n\n // Camel Case Types\n this.camelCaseTypes = options.camelCaseTypes === false\n ? string => string\n : camelCase\n\n // Resource Case\n const cases = {\n camel: camelCase,\n kebab: kebabCase,\n snake: snakeCase,\n\n default: string => string\n }\n\n this.resourceCase = cases[options.resourceCase] || cases.default\n \n // Pluralise\n this.pluralize = options.pluralize === false\n ? string => string\n : pluralize\n\n // Interceptors\n this.onResponseError = error => error\n\n // Alias\n this.fetch = this.get\n this.update = this.patch\n this.create = this.post\n this.remove = this.delete\n }\n\n #splitModel (model) {\n return splitModel(model, {\n resourceCase: this.resourceCase,\n pluralize: this.pluralize\n })\n }\n\n async request (options = {\n method: 'GET',\n headers: {}\n }) {\n const baseURL = this.baseURL || options.baseURL\n\n const url = new URL(\n options.url.startsWith('/') ? options.url.slice(1) : options.url,\n baseURL.endsWith('/') ? baseURL : baseURL + '/'\n )\n\n // Params\n if (options.params) {\n url.search = this.queryFormatter(options.params)\n }\n\n // Body\n if (options.body) {\n options.body = serialize(options.type, options.body, {\n camelCaseTypes: this.camelCaseTypes,\n pluralTypes: this.pluralize\n })\n }\n\n // Request\n const makeRequest = () => {\n // Headers\n const headers = new Headers({\n ...this.headers,\n ...options.headers\n })\n\n // Fetch\n const fetchOptions = {\n method: options.method,\n body: options.body,\n headers\n }\n\n if (typeof this.fetchFunction === 'function') {\n return this.fetchFunction(url, fetchOptions)\n }\n\n return fetch(url, fetchOptions)\n }\n\n try {\n let response = await makeRequest()\n\n if (!response.ok) {\n response.replayRequest = makeRequest\n\n const replayedResponse = await this.onResponseError(response)\n\n if (replayedResponse instanceof Response) {\n response = replayedResponse\n }\n\n if (!response.ok) {\n throw new Error(response.statusText)\n }\n }\n\n // Response Headers\n const responseHeaders = {}\n\n for (const [key, value] of response.headers.entries()) {\n responseHeaders[key] = value\n }\n\n const contentType = responseHeaders['content-type']\n\n // Response Data\n const data = contentType && contentType.includes(jsonType)\n ? await response.json()\n : {}\n\n // Return\n return {\n ...(data.errors ? data : deserialize(data)),\n\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders\n }\n } catch (error) {\n throw error\n }\n }\n\n get (model, options = { method: 'GET' }) {\n try {\n options.url = model.split('/')\n .map(part => this.resourceCase(part))\n .filter(Boolean)\n .join('/')\n\n return this.request(options)\n } catch (error) {\n throw errorParser(error)\n }\n }\n\n patch (model, body, options = { method: 'PATCH' }) {\n try {\n const [type, url] = this.#splitModel(model)\n\n return this.request({\n url: body?.id ? `${url}/${body.id}` : url,\n body,\n type,\n\n ...options\n })\n } catch (error) {\n throw errorParser(error)\n }\n }\n\n post (model, body, options = { method: 'POST' }) {\n try {\n const [type, url] = this.#splitModel(model)\n\n return this.request({\n url,\n body,\n type,\n\n ...options\n })\n } catch (error) {\n throw errorParser(error)\n }\n }\n\n delete (model, id, options = { method: 'DELETE' }) {\n try {\n const [type, url] = this.#splitModel(model)\n\n return this.request({\n url: `${url}/${id}`,\n body: { id },\n type,\n\n ...options\n })\n } catch (error) {\n throw errorParser(error)\n }\n }\n}\n", "/**\n * Deattribute JSON:API data.\n *\n * @param {Object|Object[]} data The JSON:API data to deattribute.\n * @returns {Object} The deattributed data.\n */\nexport function deattribute (data) {\n if (Array.isArray(data)) {\n return data.map(deattribute)\n }\n\n const output = {\n type: data.type,\n id: data.id\n }\n\n for (const key in data.attributes) {\n output[key] = data.attributes[key]\n }\n\n for (const key in data.relationships) {\n if (data.relationships[key].data) {\n output[key] = data.relationships[key].data\n }\n }\n\n return output\n}", "import { deattribute } from './deattribute.js'\n\n/**\n * Group included JSON:API data by type and ID.\n *\n * @param {Object[]} included The included JSON:API data.\n * @returns {Object} The grouped included data.\n */\nfunction groupIncluded (included) {\n const groups = {}\n\n for (const item of included) {\n if (!groups[item.type]) {\n groups[item.type] = {}\n }\n\n groups[item.type][item.id] = deattribute(item)\n }\n\n return groups\n}\n\n/**\n * Checks if a value is an object.\n *\n * @param {*} object The value to check.\n * @returns {boolean} Whether the value is an object.\n */\nfunction hasObject (object) {\n return typeof object === 'object' && object !== null\n}\n\n/**\n * Deserialises a JSON-API response.\n *\n * @param {Object} response The JSON-API response.\n * @returns {Object} The deserialised response.\n */\nexport function deserialize (response) {\n const output = {}\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 (response.included) {\n const included = groupIncluded(response.included)\n\n const getIncluded = item => item.type in included\n ? included[item.type][item.id]\n : item\n\n const replace = item => Array.isArray(item)\n ? item.map(getIncluded)\n : getIncluded(item)\n\n // Replace relationships with included data.\n for (const type in included) {\n for (const id in included[type]) {\n for (const key in included[type][id]) {\n const item = included[type][id][key]\n\n if (hasObject(item)) {\n included[type][id][key] = replace(item)\n }\n }\n }\n }\n\n // Replace relationships in the main data with included data.\n if (Array.isArray(output.data)) {\n for (const item of output.data) {\n for (const key in item) {\n if (hasObject(item[key])) {\n item[key] = replace(item[key])\n }\n }\n }\n } else if (hasObject(output.data)) {\n for (const key in output.data) {\n if (hasObject(output.data[key])) {\n output.data[key] = replace(output.data[key])\n }\n }\n }\n }\n\n return output\n}", "/**\n * Parse the error response from the API.\n *\n * @param {Object} error The error object.\n * @throws {Error} The parsed error object.\n */\nexport function errorParser (error) {\n if (error.response) {\n const { data } = error.response\n\n if (data?.errors) {\n error.errors = data.errors\n }\n }\n\n throw error\n}\n", "import { errorParser } from './error-parser.js'\n\n/**\n * Checks if a value is an object.\n *\n * @param {*} object The object to check.\n * @returns {boolean} Whether the value is an object.\n */\nfunction hasObject (object) {\n return typeof object === 'object' && object !== null\n}\n\n/**\n * Serialises a JSON-API request.\n * \n * @param {string} type The entity name.\n * @param {Object} request The request to serialise.\n * @param {Object} options The serialisation options.\n * @returns {string} The JSON serialised request.\n */\nexport function serialize (type, request, options = {\n camelCaseTypes: string => string,\n pluralTypes: string => string\n}) {\n const included = []\n\n function include (node, subtype) {\n if (!hasObject(node)) {\n return\n }\n\n if (!node.id) {\n throw new Error('All included resources must have an ID.')\n }\n\n if (!included.find(item => item.id === node.id)) {\n included.push(extractData(node, subtype))\n }\n }\n\n function extractData (node, subtype) {\n const data = {\n type: options.pluralTypes(options.camelCaseTypes(subtype))\n }\n\n for (const key in node) {\n if (key === 'type') {\n continue\n }\n\n if (key === 'id') {\n data.id = String(node.id)\n continue\n }\n \n const value = node[key]\n\n // Is this a relationship?\n if (typeof value === 'object') {\n data.relationships = data.relationships || {}\n\n // One-To-Many / Many-To-Many\n if (Array.isArray(value)) {\n data.relationships[key] = {\n data: value.map(item => {\n include(item, key)\n return { type: item.type || key, id: item.id }\n })\n }\n\n continue\n }\n\n // One-To-One\n data.relationships[key] = {\n data: { type: value.type || key, id: value.id }\n }\n\n include(value, key)\n\n continue\n }\n\n // Is this an attribute?\n data.attributes = data.attributes || {}\n data.attributes[key] = node[key]\n }\n\n return data\n }\n\n try {\n const data = extractData(request, type)\n return JSON.stringify({ data, included })\n } catch (error) {\n errorParser(error)\n }\n}\n", "/**\n * Loop through an object and build a query string.\n * \n * @param {URLSearchParams} query The query to append to.\n * @param {Object} object The object to loop through.\n * @param {string} prefix The prefix to use.\n * @returns {void}\n * @private\n */\nfunction buildQuery (query, object = {}, prefix = '') {\n const isArray = Array.isArray(object)\n\n for (const key in object) {\n const value = object[key]\n const withPrefix = prefix ? `${prefix}[${isArray ? '' : key}]` : key\n\n value instanceof Object\n ? buildQuery(query, value, withPrefix)\n : query.append(withPrefix, value)\n }\n}\n\n/**\n * Format query parameters.\n * \n * @param {Object} parameters The parameters to format.\n * @returns {URLSearchParams} The formatted query.\n */\nexport function queryFormatter (parameters = {}) {\n const query = new URLSearchParams()\n buildQuery(query, parameters)\n\n return query\n}\n", "/**\n * Split a model name from a URL.\n *\n * @param {string} url The URL to split.\n * @param {Object} options The options to use.\n * @returns {string[]} The model and resource.\n */\nexport function splitModel (url, options = {\n resourceCase: string => string,\n pluralize: string => string\n}) {\n const parts = url.split('/')\n const model = parts.pop()\n const resource = parts.join('/')\n\n return [\n model,\n `${resource}/${options.pluralize(options.resourceCase(model))}`\n ]\n}\n", "/**\n * Convert a string from snake_case and kebab-case to camelCase.\n *\n * @param {string} input The string to convert.\n * @returns {string} The converted string.\n */\nexport function camelCase (input) {\n return input\n .toLowerCase()\n .replace(/[-_](.)/g, (_, char) => char.toUpperCase())\n .replace(/^(.)/, (char) => char.toLowerCase())\n}\n", "/**\n * Convert a string from camelCase and snake_case to kebab-case.\n *\n * @param {string} input The string to convert.\n * @returns {string} The converted string.\n */\nexport function kebabCase (input) {\n return input\n .replace(/([a-z])([A-Z])/g, '$1-$2')\n .replace(/_/g, '-')\n .toLowerCase()\n}\n", "/**\n * Converts a string from camelCase and kebab-case to snake_case.\n *\n * @param {string} input The string to convert.\n * @returns {string} The converted string.\n */\nexport function snakeCase (input) {\n return input\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n .replace(/-/g, '_')\n .toLowerCase()\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["pluralize", "deattribute", "data", "output", "key", "groupIncluded", "included", "groups", "item", "deattribute", "hasObject", "object", "deserialize", "response", "output", "getIncluded", "replace", "type", "id", "key", "errorParser", "error", "data", "hasObject", "object", "serialize", "type", "request", "options", "string", "included", "include", "node", "subtype", "item", "extractData", "data", "key", "value", "error", "errorParser", "buildQuery", "query", "object", "prefix", "isArray", "key", "value", "withPrefix", "queryFormatter", "parameters", "splitModel", "url", "options", "string", "parts", "model", "resource", "camelCase", "input", "_", "char", "kebabCase", "input", "snakeCase", "input", "jsonType", "Fetchja", "options", "object", "queryFormatter", "string", "camelCase", "cases", "kebabCase", "snakeCase", "pluralize", "error", "#splitModel", "model", "splitModel", "baseURL", "url", "serialize", "makeRequest", "headers", "fetchOptions", "response", "replayedResponse", "responseHeaders", "key", "value", "contentType", "data", "deserialize", "part", "errorParser", "body", "type", "id"]
|
|
7
|
-
}
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fetchja",
|
|
3
|
-
"version": "
|
|
4
|
-
"main": "dist/index.js",
|
|
3
|
+
"version": "2.0.0",
|
|
5
4
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
"build": "esbuild src/index.js --bundle --format=esm --packages=external --minify --sourcemap --outfile=dist/index.js",
|
|
8
|
-
"test": "node --test tests/*"
|
|
9
|
-
},
|
|
5
|
+
"description": "A super simple, modern, lightweight, zero-dependency JSON:API client built on the Fetch API.",
|
|
10
6
|
"author": "Caio Tarifa <caio@yahoo.com>",
|
|
11
7
|
"license": "ISC",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm --dts --minify --sourcemap --clean",
|
|
23
|
+
"test": "node --import tsx --test tests/*.test.ts",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"lint": "eslint .",
|
|
26
|
+
"lint:fix": "eslint . --fix",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
15
28
|
},
|
|
16
29
|
"devDependencies": {
|
|
17
|
-
"
|
|
30
|
+
"@eslint/js": "^10.0.1",
|
|
31
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
32
|
+
"@types/node": "^25.9.1",
|
|
33
|
+
"eslint": "^10.4.1",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"tsx": "^4.7.0",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"typescript-eslint": "^8.60.0"
|
|
18
38
|
},
|
|
19
39
|
"repository": {
|
|
20
40
|
"type": "git",
|