Skip to content

Commit

Permalink
feat(http): add Http class (#400) (#407)
Browse files Browse the repository at this point in the history
close #400

---------

Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com>
Co-authored-by: Kia King Ishii <kia.king.08@gmail.com>
  • Loading branch information
brc-dd and kiaking committed Dec 7, 2023
1 parent f73ad8f commit 93a4508
Show file tree
Hide file tree
Showing 6 changed files with 1,411 additions and 789 deletions.
7 changes: 7 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ function sidebar(): DefaultTheme.SidebarItem[] {
{ text: 'Utils', link: '/composables/utils' }
]
},
{
text: 'Network Requests',
collapsed: false,
items: [
{ text: 'Http', link: '/network-requests/http' },
]
},
{
text: 'Validation',
collapsed: false,
Expand Down
139 changes: 139 additions & 0 deletions docs/network-requests/http.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Http <Badge text="3.9.0" />

`Http` module provides a set of functions for making HTTP requests.

## `Http`

The `Http` class. It uses [ofetch](https://github.com/unjs/ofetch) under the hood so that it can be smoothly used together with Nuxt.

This class deeply integrates with [Laravel Sanctum](https://laravel.com/docs/sanctum), which is the authentication system used by Laravel. When instantiating the class, it will automatically checks for cookies and set the `X-XSRF-TOKEN` header. When cookies are missing, it will automatically make a request to the Laravel Sanctum endpoint to obtain it.

```ts
import { Http } from '@globalbrain/sefirot/lib/http/Http'

const http = new Http()

const res = http.get('https://example.com')
```

### `static xsrfUrl`

Holds the static URL for the Laravel Sanctum endpoint.

```ts
class Http {
// @default '/api/csrf-cookie'
static xsrfUrl: string
}
```

### `get`

Performs a `GET` request.

```ts
import { type FetchOptions } from 'ofetch'

class Http {
get<T = any>(url: string, options?: FetchOptions): Promise<T>
}
```

### `head`

Performs a `HEAD` request.

```ts
import { type FetchOptions } from 'ofetch'

class Http {
head<T = any>(url: string, options?: FetchOptions): Promise<T>
}
```

### `post`

Performs a `POST` request.

```ts
import { type FetchOptions } from 'ofetch'

class Http {
post<T = any>(
url: string,
body?: any,
options?: FetchOptions
): Promise<T>
}
```
### `put`

Performs a `PUT` request.

```ts
import { type FetchOptions } from 'ofetch'

class Http {
put<T = any>(
url: string,
body?: any,
options?: FetchOptions
): Promise<T>
}
```

### `patch`

Performs a `PATCH` request.

```ts
import { type FetchOptions } from 'ofetch'

class Http {
patch<T = any>(
url: string,
body?: any,
options?: FetchOptions
): Promise<T>
}
```

### `delete`

Performs a `DELETE` request.

```ts
import { type FetchOptions } from 'ofetch'

class Http {
delete<T = any>(url: string, options?: FetchOptions): Promise<T>
}
```

### `upload`

Performs a `POST` request with `multipart/form-data` content type. Useful for uploading files. It also handles nested body structures as well.

```ts
import { type FetchOptions } from 'ofetch'

class Http {
upload<T = any>(
url: string,
body?: any,
options?: FetchOptions
): Promise<T>
}
```

### `download`

Download a file from the response. Use this method when you want browser to save a file to local disk.

```ts
import { type FetchOptions } from 'ofetch'

class Http {
download<T = any>(url: string, options?: FetchOptions): Promise<T>
}
```
2 changes: 1 addition & 1 deletion lib/composables/Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function useData<T extends Record<string, any>>(
}

return {
state: reactiveState,
state: reactiveState as T,
init
}
}
Expand Down
137 changes: 137 additions & 0 deletions lib/http/Http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { parse as parseContentDisposition } from '@tinyhttp/content-disposition'
import { parse as parseCookie } from '@tinyhttp/cookie'
import FileSaver from 'file-saver'
import { $fetch, type FetchOptions } from 'ofetch'
import { stringify } from 'qs'

export class Http {
static xsrfUrl = '/api/csrf-cookie'

private async ensureXsrfToken(): Promise<string | undefined> {
let xsrfToken = parseCookie(document.cookie)['XSRF-TOKEN']

if (!xsrfToken) {
await this.head(Http.xsrfUrl)
xsrfToken = parseCookie(document.cookie)['XSRF-TOKEN']
}

return xsrfToken
}

private async buildRequest(
url: string,
_options: FetchOptions = {}
): Promise<[string, FetchOptions]> {
const { method, params, query, ...options } = _options

const xsrfToken
= ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method || '') && (await this.ensureXsrfToken())

const queryString = stringify(
{ ...params, ...query },
{ arrayFormat: 'brackets', encodeValuesOnly: true }
)

return [
`${url}${queryString ? `?${queryString}` : ''}`,
{
method,
credentials: 'include',
...options,
headers: {
Accept: 'application/json',
...(xsrfToken && { 'X-XSRF-TOKEN': xsrfToken }),
...options.headers
}
}
]
}

private async performRequest<T>(url: string, options: FetchOptions = {}) {
return $fetch<T, any>(...(await this.buildRequest(url, options)))
}

private async performRequestRaw<T>(url: string, options: FetchOptions = {}) {
return $fetch.raw<T, any>(...(await this.buildRequest(url, options)))
}

private objectToFormData(obj: any, form?: FormData, namespace?: string) {
const fd = form || new FormData()
let formKey: string

for (const property in obj) {
if (Reflect.has(obj, property)) {
if (namespace) {
formKey = `${namespace}[${property}]`
} else {
formKey = property
}

if (obj[property] === undefined) {
continue
}

if (typeof obj[property] === 'object' && !(obj[property] instanceof Blob)) {
this.objectToFormData(obj[property], fd, property)
} else {
fd.append(formKey, obj[property])
}
}
}

return fd
}

async get<T = any>(url: string, options?: FetchOptions): Promise<T> {
return this.performRequest<T>(url, { method: 'GET', ...options })
}

async head<T = any>(url: string, options?: FetchOptions): Promise<T> {
return this.performRequest<T>(url, { method: 'HEAD', ...options })
}

async post<T = any>(url: string, body?: any, options?: FetchOptions): Promise<T> {
return this.performRequest<T>(url, { method: 'POST', body, ...options })
}

async put<T = any>(url: string, body?: any, options?: FetchOptions): Promise<T> {
return this.performRequest<T>(url, { method: 'PUT', body, ...options })
}

async patch<T = any>(url: string, body?: any, options?: FetchOptions): Promise<T> {
return this.performRequest<T>(url, { method: 'PATCH', body, ...options })
}

async delete<T = any>(url: string, options?: FetchOptions): Promise<T> {
return this.performRequest<T>(url, { method: 'DELETE', ...options })
}

async upload<T = any>(url: string, body?: any, options?: FetchOptions): Promise<T> {
const formData = this.objectToFormData(body)

return this.performRequest<T>(url, {
method: 'POST',
body: formData,
...options
})
}

async download(url: string, options?: FetchOptions): Promise<void> {
const { _data: blob, headers } = await this.performRequestRaw<Blob>(url, {
method: 'GET',
responseType: 'blob',
...options
})

if (!blob) {
throw new Error('No blob')
}

const { filename = 'download' }
= parseContentDisposition(headers.get('Content-Disposition') || '')?.parameters || {}

FileSaver.saveAs(blob, filename as string)
}
}

export type { FetchOptions }

0 comments on commit 93a4508

Please sign in to comment.