fetchja 1.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.js ADDED
@@ -0,0 +1,2 @@
1
+ import x from"pluralize";function n(t){if(Array.isArray(t))return t.map(n);let e={id:t.id,type:t.type};for(let r in t.attributes)e[r]=t.attributes[r];for(let r in t.relationships)t.relationships[r].data&&(e[r]=t.relationships[r].data);return e}function g(t){let e={};for(let r of t)e[r.type]||(e[r.type]={}),e[r.type][r.id]=n(r);return e}function h(t){let e={};if(t.data&&(e.data=n(t.data)),t.meta&&(e.meta=t.meta),t.included){let r=g(t.included),s=i=>r[i.type][i.id];for(let i of e.data)for(let o in i){let a=i[o];typeof a=="object"&&(i[o]=Array.isArray(a)?a.map(s):s(a))}}return e}function c(t){if(t.response){let{data:e}=t.response;e?.errors&&(t.errors=e.errors)}throw t}function k(t,e,r){return typeof t=="object"&&t!==null?(r.relationships||(r.relationships={}),r.relationships[e]={data:t.id?{id:t.id,type:t.type||e}:t,links:t.links,meta:t.meta}):(r.attributes||(r.attributes={}),r.attributes[e]=t),r}function f(t,e,r={camelCaseTypes:s=>s,pluralTypes:s=>s}){try{if(e===null||Array.isArray(e)&&!e.length)return{data:e};let s={type:r.pluralTypes(r.camelCaseTypes(t))};e.id&&(s.id=String(e.id));for(let i in e)["id","type"].includes(i)||k(e[i],i,s);return JSON.stringify({data:s})}catch(s){c(s)}}function y(t,e={},r=""){let s=Array.isArray(e);for(let i in e){let o=e[i],a=r?`${r}[${s?"":i}]`:i;o instanceof Object?y(t,o,a):t.append(a,o)}}function m(t={}){let e=new URLSearchParams;return y(e,t),e}function d(t,e={resourceCase:r=>r,pluralize:r=>r}){let r=t.split("/"),s=r.pop(),i=r.join("/");return[s,`${i}/${e.pluralize(e.resourceCase(s))}`]}function u(t){return t.toLowerCase().replace(/[-_](.)/g,(e,r)=>r.toUpperCase()).replace(/^(.)/,e=>e.toLowerCase())}function C(t){return t.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/_/g,"-").toLowerCase()}function b(t){return t.replace(/([a-z])([A-Z])/g,"$1_$2").replace(/-/g,"_").toLowerCase()}var l="application/vnd.api+json",p=class{constructor(e={headers:{}}){this.baseURL=e.baseURL,this.headers={Accept:l,"Content-Type":l,...e.headers},this.queryFormatter=typeof e.queryFormatter=="function"?e.queryFormatter:s=>m(s),this.camelCaseTypes=e.camelCaseTypes===!1?s=>s:u;let r={camel:u,kebab:C,snake:b,default:s=>s};this.resourceCase=r[e.resourceCase]||r.default,this.pluralize=e.pluralize===!1?s=>s:x,this.onResponseError=s=>s,this.fetch=this.get,this.update=this.patch,this.create=this.post,this.remove=this.delete}#e(e){return d(e,{resourceCase:this.resourceCase,pluralize:this.pluralize})}async request(e={method:"GET",headers:{}}){let r=new URL(e.url,this.baseURL||e.baseURL);e.params&&(r.search=this.queryFormatter(e.params));let s=new Headers({...this.headers,...e.headers});e.body&&(e.body=f(e.type,e.body,{camelCaseTypes:this.camelCaseTypes,pluralTypes:this.pluralize}));try{let i=await fetch(r,{method:e.method,body:e.body,headers:s});if(!i.ok)throw this.onResponseError(i),new Error(i.statusText);let o={};for(let[w,z]of i.headers.entries())o[w]=z;let a=o["content-type"],T=a&&a.includes(l)?await i.json():{};return{...h(T),status:i.status,statusText:i.statusText,headers:o}}catch(i){throw i}}get(e,r={method:"GET"}){try{return r.url=e.split("/").map(s=>this.resourceCase(s)).filter(Boolean).join("/"),this.request(r)}catch(s){throw c(s)}}patch(e,r,s={method:"PATCH"}){try{let[i,o]=this.#e(e);return this.request({url:r?.id?`${o}/${r.id}`:o,body:r,type:i,...s})}catch(i){throw c(i)}}post(e,r,s={method:"POST"}){try{let[i,o]=this.#e(e);return this.request({url:o,body:r,type:i,...s})}catch(i){throw c(i)}}delete(e,r,s={method:"DELETE"}){try{let[i,o]=this.#e(e);return this.request({url:`${o}/${r}`,body:{id:r},type:i,...s})}catch(i){throw c(i)}}};export{p as default};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
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 {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 // 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 url = new URL(options.url, this.baseURL || options.baseURL)\n\n // Params\n if (options.params) {\n url.search = this.queryFormatter(options.params)\n }\n\n // Headers\n const headers = new Headers({\n ...this.headers,\n ...options.headers\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 // Fetch\n try {\n const response = await fetch(url, {\n method: options.method,\n body: options.body,\n headers\n })\n\n if (!response.ok) {\n this.onResponseError(response)\n throw new Error(response.statusText)\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 ...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 id: data.id,\n type: data.type\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 * 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 const getIncluded = item => included[item.type][item.id]\n\n for (const item of output.data) {\n for (const key in item) {\n const itemKey = item[key]\n\n if (typeof itemKey === 'object') {\n item[key] = Array.isArray(itemKey)\n ? itemKey.map(getIncluded)\n : getIncluded(itemKey)\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\nfunction serializeNode (node, key, data) {\n if (typeof node === 'object' && node !== null) {\n if (!data.relationships) {\n data.relationships = {}\n }\n\n data.relationships[key] = {\n data: node.id ? { id: node.id, type: node.type || key } : node,\n links: node.links,\n meta: node.meta\n }\n } else {\n if (!data.attributes) {\n data.attributes = {}\n }\n\n data.attributes[key] = node\n }\n\n return data\n}\n\nexport function serialize (type, data, options = {\n camelCaseTypes: string => string,\n pluralTypes: string => string\n}) {\n try {\n if (data === null || (Array.isArray(data) && !data.length)) {\n return { data }\n }\n\n const output = {\n type: options.pluralTypes(options.camelCaseTypes(type))\n }\n\n if (data.id) {\n output.id = String(data.id)\n }\n\n for (const key in data) {\n if (['id', 'type'].includes(key)) {\n continue\n }\n\n serializeNode(data[key], key, output)\n }\n\n return JSON.stringify({ data: output })\n } catch (error) {\n errorParser(error)\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", "deserialize", "response", "output", "getIncluded", "key", "itemKey", "errorParser", "error", "data", "serializeNode", "node", "key", "data", "serialize", "type", "options", "string", "output", "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", "url", "headers", "serialize", "response", "responseHeaders", "key", "value", "contentType", "data", "deserialize", "part", "errorParser", "body", "type", "id"]
7
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "fetchja",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "esbuild src/index.js --bundle --format=esm --packages=external --minify --sourcemap --outfile=dist/index.js",
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "author": "Caio Tarifa <caio@yahoo.com>",
11
+ "license": "ISC",
12
+ "description": "The ultimate JavaScript library designed to make your JSON:API interactions seamless and intuitive.",
13
+ "dependencies": {
14
+ "pluralize": "^8.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "esbuild": "0.21.5"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/caiotarifa/fetchja.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/caiotarifa/fetchja/issues"
25
+ },
26
+ "homepage": "https://github.com/caiotarifa/fetchja#readme"
27
+ }
package/readme.md ADDED
@@ -0,0 +1,36 @@
1
+ # Fetchja
2
+
3
+ Welcome to **Fetchja**, the ultimate JavaScript library designed to make your JSON:API interactions seamless and intuitive. Inspired by the renowned [Kitsu](https://github.com/wopian/kitsu) library, Fetchja leverages the native Fetch API, ensuring a lightweight and efficient experience.
4
+
5
+ ## Why Fetchja?
6
+
7
+ - ⚡️ **Lightweight and Fast**: Built on the native Fetch API, Fetchja ensures minimal overhead and maximum performance.
8
+ - 🎨 **Intuitive Design**: Easy-to-understand methods and configurations make Fetchja accessible for developers of all levels.
9
+ - 💪 **Flexible and Customizable**: Tailor Fetchja to your needs with customizable headers, query parameters, and resource cases.
10
+
11
+ ## Installation
12
+
13
+ To get started with Fetchja, simply install it via `npm` along with the `pluralize` library:
14
+
15
+ ```bash
16
+ $ npm install fetchja pluralize
17
+ ```
18
+
19
+ ## Getting Started
20
+
21
+ Here's a quick example to get you up and running with Fetchja:
22
+
23
+ ```javascript
24
+ import { Fetchja } from 'fetchja'
25
+
26
+ const api = new Fetchja({
27
+ baseURL: 'https://api.example.com'
28
+ });
29
+
30
+ try {
31
+ const response = await api.get('/posts')
32
+ console.log(response)
33
+ } catch (error) {
34
+ console.error(error)
35
+ }
36
+ ```
package/src/index.js ADDED
@@ -0,0 +1,214 @@
1
+ import pluralize from 'pluralize'
2
+
3
+ import { deserialize } from './utils/deserialize.js'
4
+ import { serialize } from './utils/serialize.js'
5
+
6
+ import { errorParser } from './utils/error-parser.js'
7
+ import { queryFormatter } from './utils/query-formatter.js'
8
+ import { splitModel } from './utils/split-model.js'
9
+
10
+ import { camelCase } from './utils/camel-case.js'
11
+ import { kebabCase } from './utils/kebab-case.js'
12
+ import { snakeCase } from './utils/snake-case.js'
13
+
14
+ const jsonType = 'application/vnd.api+json'
15
+
16
+ /**
17
+ * Options for Fetchja.
18
+ *
19
+ * @typedef {Object} FetchjaOptions
20
+ * @property {string} baseURL The base URL for all requests.
21
+ * @property {Object} headers The headers to include in all requests.
22
+ * @property {Function} queryFormatter A function to format query parameters.
23
+ * @property {string} resourceCase The case to use for resource names.
24
+ * @property {boolean} pluralize Pluralize resource names.
25
+ */
26
+
27
+ /**
28
+ * Fetchja is a simple wrapper around the Fetch API.
29
+ *
30
+ * @class Fetchja
31
+ * @param {FetchjaOptions} [options] Options for Fetchja.
32
+ */
33
+ export default class Fetchja {
34
+ constructor (options = {
35
+ headers: {}
36
+ }) {
37
+ this.baseURL = options.baseURL
38
+
39
+ // Headers
40
+ this.headers = {
41
+ Accept: jsonType,
42
+ 'Content-Type': jsonType,
43
+ ...options.headers
44
+ }
45
+
46
+ // Query
47
+ this.queryFormatter = typeof options.queryFormatter === 'function'
48
+ ? options.queryFormatter
49
+ : object => queryFormatter(object)
50
+
51
+ // Camel Case Types
52
+ this.camelCaseTypes = options.camelCaseTypes === false
53
+ ? string => string
54
+ : camelCase
55
+
56
+ // Resource Case
57
+ const cases = {
58
+ camel: camelCase,
59
+ kebab: kebabCase,
60
+ snake: snakeCase,
61
+
62
+ default: string => string
63
+ }
64
+
65
+ this.resourceCase = cases[options.resourceCase] || cases.default
66
+
67
+ // Pluralise
68
+ this.pluralize = options.pluralize === false
69
+ ? string => string
70
+ : pluralize
71
+
72
+ // Interceptors
73
+ this.onResponseError = error => error
74
+
75
+ // Alias
76
+ this.fetch = this.get
77
+ this.update = this.patch
78
+ this.create = this.post
79
+ this.remove = this.delete
80
+ }
81
+
82
+ #splitModel (model) {
83
+ return splitModel(model, {
84
+ resourceCase: this.resourceCase,
85
+ pluralize: this.pluralize
86
+ })
87
+ }
88
+
89
+ async request (options = {
90
+ method: 'GET',
91
+ headers: {}
92
+ }) {
93
+ const url = new URL(options.url, this.baseURL || options.baseURL)
94
+
95
+ // Params
96
+ if (options.params) {
97
+ url.search = this.queryFormatter(options.params)
98
+ }
99
+
100
+ // Headers
101
+ const headers = new Headers({
102
+ ...this.headers,
103
+ ...options.headers
104
+ })
105
+
106
+ // Body
107
+ if (options.body) {
108
+ options.body = serialize(options.type, options.body, {
109
+ camelCaseTypes: this.camelCaseTypes,
110
+ pluralTypes: this.pluralize
111
+ })
112
+ }
113
+
114
+ // Fetch
115
+ try {
116
+ const response = await fetch(url, {
117
+ method: options.method,
118
+ body: options.body,
119
+ headers
120
+ })
121
+
122
+ if (!response.ok) {
123
+ this.onResponseError(response)
124
+ throw new Error(response.statusText)
125
+ }
126
+
127
+ // Response Headers
128
+ const responseHeaders = {}
129
+
130
+ for (const [key, value] of response.headers.entries()) {
131
+ responseHeaders[key] = value
132
+ }
133
+
134
+ const contentType = responseHeaders['content-type']
135
+
136
+ // Response Data
137
+ const data = contentType && contentType.includes(jsonType)
138
+ ? await response.json()
139
+ : {}
140
+
141
+ // Return
142
+ return {
143
+ ...deserialize(data),
144
+
145
+ status: response.status,
146
+ statusText: response.statusText,
147
+ headers: responseHeaders
148
+ }
149
+ } catch (error) {
150
+ throw error
151
+ }
152
+ }
153
+
154
+ get (model, options = { method: 'GET' }) {
155
+ try {
156
+ options.url = model.split('/')
157
+ .map(part => this.resourceCase(part))
158
+ .filter(Boolean)
159
+ .join('/')
160
+
161
+ return this.request(options)
162
+ } catch (error) {
163
+ throw errorParser(error)
164
+ }
165
+ }
166
+
167
+ patch (model, body, options = { method: 'PATCH' }) {
168
+ try {
169
+ const [type, url] = this.#splitModel(model)
170
+
171
+ return this.request({
172
+ url: body?.id ? `${url}/${body.id}` : url,
173
+ body,
174
+ type,
175
+
176
+ ...options
177
+ })
178
+ } catch (error) {
179
+ throw errorParser(error)
180
+ }
181
+ }
182
+
183
+ post (model, body, options = { method: 'POST' }) {
184
+ try {
185
+ const [type, url] = this.#splitModel(model)
186
+
187
+ return this.request({
188
+ url,
189
+ body,
190
+ type,
191
+
192
+ ...options
193
+ })
194
+ } catch (error) {
195
+ throw errorParser(error)
196
+ }
197
+ }
198
+
199
+ delete (model, id, options = { method: 'DELETE' }) {
200
+ try {
201
+ const [type, url] = this.#splitModel(model)
202
+
203
+ return this.request({
204
+ url: `${url}/${id}`,
205
+ body: { id },
206
+ type,
207
+
208
+ ...options
209
+ })
210
+ } catch (error) {
211
+ throw errorParser(error)
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Convert a string from snake_case and kebab-case to camelCase.
3
+ *
4
+ * @param {string} input The string to convert.
5
+ * @returns {string} The converted string.
6
+ */
7
+ export function camelCase (input) {
8
+ return input
9
+ .toLowerCase()
10
+ .replace(/[-_](.)/g, (_, char) => char.toUpperCase())
11
+ .replace(/^(.)/, (char) => char.toLowerCase())
12
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Deattribute JSON:API data.
3
+ *
4
+ * @param {Object|Object[]} data The JSON:API data to deattribute.
5
+ * @returns {Object} The deattributed data.
6
+ */
7
+ export function deattribute (data) {
8
+ if (Array.isArray(data)) {
9
+ return data.map(deattribute)
10
+ }
11
+
12
+ const output = {
13
+ id: data.id,
14
+ type: data.type
15
+ }
16
+
17
+ for (const key in data.attributes) {
18
+ output[key] = data.attributes[key]
19
+ }
20
+
21
+ for (const key in data.relationships) {
22
+ if (data.relationships[key].data) {
23
+ output[key] = data.relationships[key].data
24
+ }
25
+ }
26
+
27
+ return output
28
+ }
@@ -0,0 +1,58 @@
1
+ import { deattribute } from './deattribute.js'
2
+
3
+ /**
4
+ * Group included JSON:API data by type and ID.
5
+ *
6
+ * @param {Object[]} included The included JSON:API data.
7
+ * @returns {Object} The grouped included data.
8
+ */
9
+ function groupIncluded (included) {
10
+ const groups = {}
11
+
12
+ for (const item of included) {
13
+ if (!groups[item.type]) {
14
+ groups[item.type] = {}
15
+ }
16
+
17
+ groups[item.type][item.id] = deattribute(item)
18
+ }
19
+
20
+ return groups
21
+ }
22
+
23
+ /**
24
+ * Deserialises a JSON-API response.
25
+ *
26
+ * @param {Object} response The JSON-API response.
27
+ * @returns {Object} The deserialised response.
28
+ */
29
+ export function deserialize (response) {
30
+ const output = {}
31
+
32
+ if (response.data) {
33
+ output.data = deattribute(response.data)
34
+ }
35
+
36
+ if (response.meta) {
37
+ output.meta = response.meta
38
+ }
39
+
40
+ if (response.included) {
41
+ const included = groupIncluded(response.included)
42
+ const getIncluded = item => included[item.type][item.id]
43
+
44
+ for (const item of output.data) {
45
+ for (const key in item) {
46
+ const itemKey = item[key]
47
+
48
+ if (typeof itemKey === 'object') {
49
+ item[key] = Array.isArray(itemKey)
50
+ ? itemKey.map(getIncluded)
51
+ : getIncluded(itemKey)
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ return output
58
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Parse the error response from the API.
3
+ *
4
+ * @param {Object} error The error object.
5
+ * @throws {Error} The parsed error object.
6
+ */
7
+ export function errorParser (error) {
8
+ if (error.response) {
9
+ const { data } = error.response
10
+
11
+ if (data?.errors) {
12
+ error.errors = data.errors
13
+ }
14
+ }
15
+
16
+ throw error
17
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Convert a string from camelCase and snake_case to kebab-case.
3
+ *
4
+ * @param {string} input The string to convert.
5
+ * @returns {string} The converted string.
6
+ */
7
+ export function kebabCase (input) {
8
+ return input
9
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
10
+ .replace(/_/g, '-')
11
+ .toLowerCase()
12
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Loop through an object and build a query string.
3
+ *
4
+ * @param {URLSearchParams} query The query to append to.
5
+ * @param {Object} object The object to loop through.
6
+ * @param {string} prefix The prefix to use.
7
+ * @returns {void}
8
+ * @private
9
+ */
10
+ function buildQuery (query, object = {}, prefix = '') {
11
+ const isArray = Array.isArray(object)
12
+
13
+ for (const key in object) {
14
+ const value = object[key]
15
+ const withPrefix = prefix ? `${prefix}[${isArray ? '' : key}]` : key
16
+
17
+ value instanceof Object
18
+ ? buildQuery(query, value, withPrefix)
19
+ : query.append(withPrefix, value)
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Format query parameters.
25
+ *
26
+ * @param {Object} parameters The parameters to format.
27
+ * @returns {URLSearchParams} The formatted query.
28
+ */
29
+ export function queryFormatter (parameters = {}) {
30
+ const query = new URLSearchParams()
31
+ buildQuery(query, parameters)
32
+
33
+ return query
34
+ }
@@ -0,0 +1,54 @@
1
+ import { errorParser } from './error-parser.js'
2
+
3
+ function serializeNode (node, key, data) {
4
+ if (typeof node === 'object' && node !== null) {
5
+ if (!data.relationships) {
6
+ data.relationships = {}
7
+ }
8
+
9
+ data.relationships[key] = {
10
+ data: node.id ? { id: node.id, type: node.type || key } : node,
11
+ links: node.links,
12
+ meta: node.meta
13
+ }
14
+ } else {
15
+ if (!data.attributes) {
16
+ data.attributes = {}
17
+ }
18
+
19
+ data.attributes[key] = node
20
+ }
21
+
22
+ return data
23
+ }
24
+
25
+ export function serialize (type, data, options = {
26
+ camelCaseTypes: string => string,
27
+ pluralTypes: string => string
28
+ }) {
29
+ try {
30
+ if (data === null || (Array.isArray(data) && !data.length)) {
31
+ return { data }
32
+ }
33
+
34
+ const output = {
35
+ type: options.pluralTypes(options.camelCaseTypes(type))
36
+ }
37
+
38
+ if (data.id) {
39
+ output.id = String(data.id)
40
+ }
41
+
42
+ for (const key in data) {
43
+ if (['id', 'type'].includes(key)) {
44
+ continue
45
+ }
46
+
47
+ serializeNode(data[key], key, output)
48
+ }
49
+
50
+ return JSON.stringify({ data: output })
51
+ } catch (error) {
52
+ errorParser(error)
53
+ }
54
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Converts a string from camelCase and kebab-case to snake_case.
3
+ *
4
+ * @param {string} input The string to convert.
5
+ * @returns {string} The converted string.
6
+ */
7
+ export function snakeCase (input) {
8
+ return input
9
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
10
+ .replace(/-/g, '_')
11
+ .toLowerCase()
12
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Split a model name from a URL.
3
+ *
4
+ * @param {string} url The URL to split.
5
+ * @param {Object} options The options to use.
6
+ * @returns {string[]} The model and resource.
7
+ */
8
+ export function splitModel (url, options = {
9
+ resourceCase: string => string,
10
+ pluralize: string => string
11
+ }) {
12
+ const parts = url.split('/')
13
+ const model = parts.pop()
14
+ const resource = parts.join('/')
15
+
16
+ return [
17
+ model,
18
+ `${resource}/${options.pluralize(options.resourceCase(model))}`
19
+ ]
20
+ }