@beyonk/http 12.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.
@@ -0,0 +1,60 @@
1
+ name: publish
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - '*'
7
+ tags:
8
+ - 'v*'
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ with:
16
+ ref: master
17
+
18
+ - uses: volta-cli/action@v4
19
+
20
+ - name: Cache pnpm modules
21
+ uses: actions/cache@v2
22
+ with:
23
+ path: ~/.pnpm-store
24
+ key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
25
+ restore-keys: |
26
+ ${{ runner.os }}-
27
+
28
+ - uses: pnpm/action-setup@v2.2.4
29
+ with:
30
+ run_install: true
31
+
32
+ - run: pnpm lint
33
+
34
+ publish-npm:
35
+ if: startsWith(github.ref, 'refs/tags/v')
36
+ needs: build
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/checkout@v3
40
+ with:
41
+ ref: master
42
+
43
+ - uses: volta-cli/action@v4
44
+
45
+ - name: Authorize NPM
46
+ run: npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
47
+
48
+ - name: Cache pnpm modules
49
+ uses: actions/cache@v2
50
+ with:
51
+ path: ~/.pnpm-store
52
+ key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
53
+ restore-keys: |
54
+ ${{ runner.os }}-
55
+
56
+ - uses: pnpm/action-setup@v2.2.4
57
+ with:
58
+ run_install: true
59
+
60
+ - run: pnpm publish
package/README.MD ADDED
@@ -0,0 +1,301 @@
1
+ <a href="https://beyonk.com">
2
+ <img src="https://user-images.githubusercontent.com/218949/144224348-1b3a20d5-d68e-4a7a-b6ac-6946f19f4a86.png" width="198" />
3
+ </a>
4
+
5
+ ## Http
6
+
7
+ [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) ![publish](https://github.com/beyonk-adventures/http/workflows/publish/badge.svg?branch=master)
8
+
9
+ Isomorphic fetch library.
10
+ <br />
11
+ Formerly known as `@beyonk/sapper-httpclient`
12
+
13
+ ## Why
14
+
15
+ In hybrid applications, there are three different ways of fetching data:
16
+
17
+ * client
18
+ * server
19
+ * isomorphic (client + server)
20
+ * retries (on various network errors)
21
+
22
+ This library helps you abstract over where you are fetching data, meaning that your code maintains consistency without having to worry about where your data is being fetched.
23
+
24
+ The way it does this is by trying to use the first available fetch method, and failing over to alternatives if a method is not available. The methods it tries are, in the following order:
25
+
26
+ 1. Any fetch library you pass to `create()` (for example, `load`'s `fetch`)
27
+ 1. window.fetch if the library detects it is running clientside
28
+ 1. `node-fetch`, or whatever you want to pass in, if nothing else is available (pure server-side)
29
+
30
+ Generally this means that your usage is the same no matter where you call it, with one exception - using this library in the `load` method requires you to pass in SvelteKit's special `fetch` method, as it is not available outside of the load method. Examples of which are below.
31
+
32
+ ## Usage
33
+
34
+ ### To use within an application:
35
+
36
+ ### Install it
37
+
38
+ ```bash
39
+ npm i -D @beyonk/http
40
+ ```
41
+
42
+ ### Configure it (both server-side and client-side as there are two bundles)
43
+
44
+ ```js
45
+ // src/client.js && src/server.js
46
+ import Api from '@beyonk/http'
47
+
48
+ Api.configure({ baseUrl: 'https://example.com/your/api/base' })
49
+ ```
50
+
51
+ ### Use it on the client:
52
+
53
+ ```js
54
+ // src/routes/some-route.html
55
+ import { create } from '@beyonk/http'
56
+
57
+ // in a method (client-side)
58
+ const api = create()
59
+ const json = await api.endpoint('some/endpoint').get()
60
+ console.log(json)
61
+
62
+ // in load (isomorphic)
63
+ const api = create()
64
+ const json = await api
65
+ .context({ fetch }) // Pass in the "fetch" parameter from load
66
+ .endpoint('some/endpoint')
67
+ .get()
68
+ console.log(json)
69
+ ```
70
+
71
+ ### Use it on the server:
72
+
73
+ ```js
74
+ // src/routes/+page.server.js
75
+ import fetch from 'node-fetch' // or SvelteKit's built in fetch
76
+ import { create } from '@beyonk/http'
77
+
78
+ const api = create()
79
+ const json = await api
80
+ .context({ fetch }) // pass node fetch in here.
81
+ .endpoint('some/endpoint')
82
+ .get()
83
+ console.log(json)
84
+ ```
85
+
86
+ ### Handling the response
87
+
88
+ ```js
89
+ import { create } from '@beyonk/http'
90
+
91
+ const api = create()
92
+ const json = await api
93
+ .endpoint('some/endpoint')
94
+ .get((json, httpStatus) => {
95
+ console.log('json response is', json)
96
+ console.log('http status code is', httpStatus)
97
+ })
98
+ ```
99
+
100
+ ## Methods
101
+
102
+ ```js
103
+ const api = create()
104
+ const client = api
105
+ .endpoint('some/endpoint')
106
+
107
+ console.log(await client.get()) // Get endpoint
108
+ console.log(await client.payload({ foo: 'bar' }).put()) // Put with body
109
+ console.log(await client.payload({ foo: 'bar' }).post()) // Post with body
110
+ console.log(await client.query({ foo: 'bar' }).get()) // Get with query
111
+ console.log(await client.del()) // Delete
112
+ console.log(await client.headers({ foo: 'bar' }).put()) // Put with headers
113
+ ```
114
+
115
+ #### client.query
116
+
117
+ The `query` method accepts an object of params as either a String or Array of Strings.
118
+ If any property passed into the query is `undefined` it will be ignored.
119
+
120
+ ```js
121
+ const api = create()
122
+ const client = api
123
+ .endpoint('some/endpoint')
124
+
125
+ console.log(await client.query({ foo: 'bar' }).get()) // will make a GET request to 'some/endpoint?foo=bar'
126
+ console.log(await client.query({ foo: 'bar', baz: 'qux' }).get()) // will make a GET request to 'some/endpoint?foo=bar&baz=qux
127
+ console.log(await client.query({ foo: ['bar', 'qux' ] }).get()) // will make a GET request to 'some/endpoint?foo=bar&foo=qux
128
+ console.log(await client.query({ foo: undefined, baz: 'qux' }).get()) // will make a GET request to 'some/endpoint?baz=qux
129
+
130
+ ```
131
+
132
+ ## Using built in response handling
133
+
134
+ ```js
135
+ const api = create()
136
+ const profile = await api
137
+ .endpoint('some/endpoint')
138
+ .get(json => {
139
+ return json.profile
140
+ })
141
+ console.log(profile)
142
+ ```
143
+
144
+ ## Catching errors
145
+
146
+ ### Per request
147
+
148
+ If no local error handler is specified, the fallback handler `default` is called. If this isn't specified, the error is logged to the console.
149
+
150
+ ```js
151
+ await client
152
+ .endpoint('some/url')
153
+ .forbidden(e => {
154
+ console.error('Forbidden', e)
155
+ })
156
+ .gone(e => {
157
+ console.error('Gone', e)
158
+ })
159
+ .notFound(e => {
160
+ console.error('Not found', e)
161
+ })
162
+ .accessDenied(e => {
163
+ console.error('Access denied', e)
164
+ })
165
+ .conflict(e => {
166
+ console.error('Conflict', e)
167
+ })
168
+ .paymentRequired(e => {
169
+ console.error('Payment Required', e)
170
+ })
171
+ .preconditionFailed(e => {
172
+ console.error('Precondition failed', e)
173
+ })
174
+ .badData(e => {
175
+ console.error('Bad data', e)
176
+ })
177
+ .default(e => {
178
+ // Any other error caught here
179
+ console.error('Some error', e)
180
+ })
181
+ .get()
182
+ ```
183
+
184
+ #### Handler signature
185
+
186
+ Handlers have a signature with two items:
187
+
188
+ ```js
189
+ .badData((e, ctx) => {
190
+ console.error('Bad data', e)
191
+ ctx.redirect('/foo/bar')
192
+ // or
193
+ ctx.error('/foo/bar')
194
+ })
195
+ ```
196
+
197
+ ctx can be whatever you want really - it is whatever you pass in as `context(...)`.
198
+
199
+ However, if the `context` object you pass in has a `fetch` function, this is used as the `fetch` for XHR requests.
200
+
201
+ ```js
202
+ export async function preload () {
203
+ await Api
204
+ .context(this) // fetch, redirect, error.
205
+ .endpoint('foo/bar')
206
+ ...
207
+ }
208
+ ```
209
+
210
+ you can also pass in other things to the context:
211
+
212
+ ```js
213
+ export async function preload () {
214
+ await Api
215
+ .context({ ...this, baz: 'qux' })
216
+ .endpoint('foo/bar')
217
+ ...
218
+ }
219
+ ```
220
+
221
+ ### At a global level
222
+
223
+ Request local error handlers override global error handlers, but if a local error handler are not specified, these will be called instead, if it exists.
224
+
225
+ Names are the same as the local handlers:
226
+
227
+ ```js
228
+ import Api from '@beyonk/http'
229
+ import { redirect } from '@sveltejs/kit'
230
+
231
+ Api.configure({
232
+ baseUrl: 'https://example.com/your/api/base',
233
+ handlers: {
234
+ paymentRequired(e => {
235
+ redirect(307, '/checkout')
236
+ })
237
+ }
238
+ })
239
+
240
+ await client
241
+ .endpoint('some/url')
242
+ .get()
243
+ ```
244
+
245
+ ## Retries
246
+
247
+ The http client can retry if a network error is encountered. The default is `retry: false`, and requests won't be retried.
248
+
249
+ Configure it as follows:
250
+
251
+ ```js
252
+ import Api from '@beyonk/http'
253
+
254
+ Api.configure({
255
+ retry: {
256
+ attempts: 3 // How many times to retry before giving up
257
+ errors: [ 'ECONNRESET' ] // A list of error codes
258
+ }
259
+ })
260
+ ```
261
+
262
+ errors is an array of any number of the [nodejs network error codes](https://nodejs.org/api/errors.html#errors_common_system_errors)
263
+
264
+
265
+ ## Parsing error payloads
266
+
267
+ As of v7.0.0 the library defaults to parsing error payloads as JSON. This means you can use the data returned in your response.
268
+
269
+ ```js
270
+ /** endpoint returns 401 with:
271
+ {
272
+ username: 'Naughty User'
273
+ }
274
+ **/
275
+
276
+ await client
277
+ .endpoint('some/url')
278
+ .accessDenied(e => {
279
+ console.error('You are not allowed', e.body.username)
280
+ })
281
+ ```
282
+
283
+ To turn this behaviour off, pass the option `parseErrors` with value false:
284
+
285
+ ```js
286
+ import Api from '@beyonk/http'
287
+
288
+ Api.configure({
289
+ parseErrors: false
290
+ })
291
+ ```
292
+
293
+ ## Running Tests
294
+
295
+ ```sh
296
+ npm test
297
+ ```
298
+
299
+ ## Credits
300
+
301
+ * Original code by [Antony Jones](https://github.com/antony)
@@ -0,0 +1,12 @@
1
+ function compose (...fns) {
2
+ return function () {
3
+ var result = fns[0].apply(this, arguments)
4
+ var len = fns.length
5
+ for (var i = 1; i < len; i++) {
6
+ result = fns[i].call(this, result)
7
+ }
8
+ return result
9
+ }
10
+ }
11
+
12
+ export { compose }
@@ -0,0 +1,231 @@
1
+ import { AccessDeniedMixin } from '../error-handlers/access-denied.js'
2
+ import { ConflictMixin } from '../error-handlers/conflict.js'
3
+ import { DefaultMixin } from '../error-handlers/default.js'
4
+ import { ForbiddenMixin } from '../error-handlers/forbidden.js'
5
+ import { HandleMixin } from '../error-handlers/handle.js'
6
+ import { NotFoundMixin } from '../error-handlers/not-found.js'
7
+ import { BadDataMixin } from '../error-handlers/bad-data.js'
8
+ import { PaymentRequiredMixin } from '../error-handlers/payment-required.js'
9
+ import { PreconditionFailedMixin } from '../error-handlers/precondition-failed.js'
10
+ import { TooManyRequestsMixin } from '../error-handlers/too-many-requests.js'
11
+ import { NotAcceptableMixin } from '../error-handlers/not-acceptable.js'
12
+ import { GoneMixin } from '../error-handlers/gone.js'
13
+ import { byCode } from '../errors.js'
14
+ import { compose } from './_just-compose.js'
15
+
16
+ class ApiBase {}
17
+ const Behaviours = compose(
18
+ AccessDeniedMixin,
19
+ ConflictMixin,
20
+ DefaultMixin,
21
+ ForbiddenMixin,
22
+ HandleMixin,
23
+ NotFoundMixin,
24
+ BadDataMixin,
25
+ PaymentRequiredMixin,
26
+ PreconditionFailedMixin,
27
+ TooManyRequestsMixin,
28
+ NotAcceptableMixin,
29
+ GoneMixin
30
+ )(ApiBase)
31
+
32
+ class Api extends Behaviours {
33
+ constructor (options) {
34
+ super()
35
+ this.options = Object.assign({
36
+ retry: false,
37
+ parseErrors: true,
38
+ handlers: {}
39
+ }, options)
40
+
41
+ this.handlers = {}
42
+ this.client = null
43
+ this.resetRequest()
44
+ }
45
+
46
+ resetRequest () {
47
+ this.config = {
48
+ endpoint: null,
49
+ method: 'get',
50
+ payload: null,
51
+ query: null,
52
+ headers: {},
53
+ overrides: {}
54
+ }
55
+ }
56
+
57
+ getClient () {
58
+ if (this.options.mock) {
59
+ console.warn('@beyonk/http: Using mocked http client')
60
+ return this.options.mock
61
+ }
62
+
63
+ if (this.client) {
64
+ return this.client
65
+ } else if (typeof window !== 'undefined') {
66
+ return window.fetch.bind(window)
67
+ }
68
+
69
+ throw Error("No client provided and can't find one automatically")
70
+ }
71
+
72
+ async send (fn) {
73
+ const endpoint = this.config.endpoint.includes('://') ? this.config.endpoint : `${this.options.baseUrl}/${this.config.endpoint}`
74
+ const hasPayload = !!this.config.payload
75
+
76
+ const options = Object.assign(
77
+ {
78
+ method: this.config.method,
79
+ cors: true,
80
+ credentials: 'include',
81
+ headers: Object.assign(
82
+ { Accept: 'application/json' },
83
+ hasPayload ? { 'Content-Type': 'application/json' } : {},
84
+ this.config.headers
85
+ )
86
+ },
87
+ hasPayload ? { body: JSON.stringify(this.config.payload) } : {},
88
+ this.config.overrides
89
+ )
90
+
91
+ const client = this.getClient()
92
+ const ep = this.config.query ? `${endpoint}?${this.config.query}` : `${endpoint}`
93
+
94
+ let result
95
+ try {
96
+ result = await this._doQuery(1, client, ep, options)
97
+ } catch (e) {
98
+ console.log(e)
99
+ return this.handle(e, this.ctx)
100
+ } finally {
101
+ this.resetRequest()
102
+ }
103
+
104
+ const { json, httpStatus } = result
105
+ return fn ? fn(json, httpStatus) : json
106
+ }
107
+
108
+ _hasContent (response) {
109
+ const contentLength = parseInt(response.headers.get('content-length'), 10)
110
+
111
+ const isNoContentResponse = response.status === 204
112
+ if (isNoContentResponse) {
113
+ return false
114
+ }
115
+
116
+ const headerExists = !isNaN(contentLength)
117
+ return !headerExists || contentLength > 0
118
+ }
119
+
120
+ async _doQuery (attempt, client, endpoint, options) {
121
+ const retry = this.options.retry || { attempts: 1 }
122
+ try {
123
+ const r = await client(endpoint, options)
124
+
125
+ if (r.status >= 200 && r.status < 400) {
126
+ let json
127
+
128
+ if (this._hasContent(r)) {
129
+ try {
130
+ json = await r.json()
131
+ } catch (e) {
132
+ console.error('Unable to parse response json', e.message)
133
+ }
134
+ }
135
+
136
+ return { httpStatus: r.status, json }
137
+ }
138
+
139
+ let content = ''
140
+ try {
141
+ content = this.options.parseErrors ? await r.json() : await r.text()
142
+ } catch (e) {
143
+ console.log('Failed to parse error body when asked.')
144
+ }
145
+
146
+ const ClientError = byCode(r.status)
147
+ throw new ClientError(r.statusText, content)
148
+ } catch (e) {
149
+ if (attempt < retry.attempts && retry.errors.includes(e.code)) {
150
+ console.warn(`Got ${e.code} when calling ${endpoint}. Retrying request (${attempt}/${retry.attempts})`)
151
+ return this._doQuery(++attempt, client, endpoint, options)
152
+ }
153
+
154
+ throw e
155
+ }
156
+ }
157
+
158
+ context (ctx) {
159
+ if (ctx.fetch) {
160
+ this.client = ctx.fetch
161
+ }
162
+ this.ctx = ctx
163
+ return this
164
+ }
165
+
166
+ override (override) {
167
+ this.config.overrides = override
168
+ return this
169
+ }
170
+
171
+ headers (headers) {
172
+ this.config.headers = headers
173
+ return this
174
+ }
175
+
176
+ async get (fn) {
177
+ this.config.method = 'GET'
178
+ return this.send(fn)
179
+ }
180
+
181
+ async post (fn) {
182
+ this.config.method = 'POST'
183
+ return this.send(fn)
184
+ }
185
+
186
+ async patch (fn) {
187
+ this.config.method = 'PATCH'
188
+ return this.send(fn)
189
+ }
190
+
191
+ async put (fn) {
192
+ this.config.method = 'PUT'
193
+ return this.send(fn)
194
+ }
195
+
196
+ async del (fn) {
197
+ this.config.method = 'DELETE'
198
+ return this.send(fn)
199
+ }
200
+
201
+ endpoint (endpoint) {
202
+ this.config.endpoint = endpoint
203
+ return this
204
+ }
205
+
206
+ query (query) {
207
+ const q = Object.entries(query).reduce((curr, [ k, v ]) => {
208
+ if (typeof v === 'undefined') {
209
+ return curr
210
+ }
211
+ if (Array.isArray(v)) {
212
+ curr.push(...v.map(n => `${k}=${encodeURIComponent(n)}`))
213
+ } else {
214
+ curr.push(`${k}=${encodeURIComponent(v)}`)
215
+ }
216
+ return curr
217
+ }, [])
218
+
219
+ this.config.query = q.join('&')
220
+ return this
221
+ }
222
+
223
+ payload (payload) {
224
+ this.config.payload = payload
225
+ return this
226
+ }
227
+ }
228
+
229
+ export {
230
+ Api
231
+ }