Skip to content

Commit 0fefe08

Browse files
committed
feat: new invoke syntax
1 parent 06e74b4 commit 0fefe08

File tree

4 files changed

+161
-83
lines changed

4 files changed

+161
-83
lines changed

src/FunctionsClient.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { resolveFetch } from './helper'
2+
import {
3+
Fetch,
4+
FunctionsFetchError,
5+
FunctionsHttpError,
6+
FunctionsRelayError,
7+
FunctionsResponse,
8+
} from './types'
9+
10+
export class FunctionsClient {
11+
protected url: string
12+
protected headers: Record<string, string>
13+
protected fetch: Fetch
14+
15+
constructor(
16+
url: string,
17+
{
18+
headers = {},
19+
customFetch,
20+
}: {
21+
headers?: Record<string, string>
22+
customFetch?: Fetch
23+
} = {}
24+
) {
25+
this.url = url
26+
this.headers = headers
27+
this.fetch = resolveFetch(customFetch)
28+
}
29+
30+
/**
31+
* Updates the authorization header
32+
* @params token - the new jwt token sent in the authorisation header
33+
*/
34+
setAuth(token: string) {
35+
this.headers.Authorization = `Bearer ${token}`
36+
}
37+
38+
/**
39+
* Invokes a function
40+
* @param functionName - the name of the function to invoke
41+
* @param functionArgs - the arguments to the function
42+
* @param options - function invoke options
43+
* @param options.headers - headers to send with the request
44+
*/
45+
async invoke(
46+
functionName: string,
47+
functionArgs: any,
48+
{
49+
headers = {},
50+
}: {
51+
headers?: Record<string, string>
52+
} = {}
53+
): Promise<FunctionsResponse> {
54+
try {
55+
let _headers: Record<string, string> = {}
56+
let body: any
57+
if (functionArgs instanceof Blob || functionArgs instanceof ArrayBuffer) {
58+
// will work for File as File inherits Blob
59+
// also works for ArrayBuffer as it is the same underlying structure as a Blob
60+
_headers['Content-Type'] = 'application/octet-stream'
61+
body = functionArgs
62+
} else if (typeof functionArgs === 'string') {
63+
// plain string
64+
_headers['Content-Type'] = 'text/plain'
65+
body = functionArgs
66+
} else if (functionArgs instanceof FormData) {
67+
// don't set content-type headers
68+
// Request will automatically add the right boundary value
69+
body = functionArgs
70+
} else {
71+
// default, assume this is JSON
72+
_headers['Content-Type'] = 'application/json'
73+
body = JSON.stringify(functionArgs)
74+
}
75+
76+
const response = await this.fetch(`${this.url}/${functionName}`, {
77+
method: 'POST',
78+
// headers priority is (high to low):
79+
// 1. invoke-level headers
80+
// 2. client-level headers
81+
// 3. default Content-Type header
82+
headers: { ..._headers, ...this.headers, ...headers },
83+
body,
84+
}).catch((fetchError) => {
85+
throw new FunctionsFetchError(fetchError)
86+
})
87+
88+
const isRelayError = response.headers.get('x-relay-error')
89+
if (isRelayError && isRelayError === 'true') {
90+
throw new FunctionsRelayError(response)
91+
}
92+
93+
if (!response.ok) {
94+
throw new FunctionsHttpError(response)
95+
}
96+
97+
let responseType = (response.headers.get('Content-Type') ?? 'text/plain').split(';')[0].trim()
98+
let data: any
99+
if (responseType === 'application/json') {
100+
data = await response.json()
101+
} else if (responseType === 'application/octet-stream') {
102+
data = await response.blob()
103+
} else if (responseType === 'multipart/form-data') {
104+
data = await response.formData()
105+
} else {
106+
// default to text
107+
data = await response.text()
108+
}
109+
110+
return { data, error: null }
111+
} catch (error) {
112+
return { data: null, error }
113+
}
114+
}
115+
}

src/helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
type Fetch = typeof fetch
1+
import { Fetch } from './types'
22

33
export const resolveFetch = (customFetch?: Fetch): Fetch => {
44
let _fetch: Fetch

src/index.ts

Lines changed: 8 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,8 @@
1-
import { resolveFetch } from './helper'
2-
import { Fetch, FunctionInvokeOptions } from './types'
3-
4-
export class FunctionsClient {
5-
protected url: string
6-
protected headers: Record<string, string>
7-
protected fetch: Fetch
8-
9-
constructor(
10-
url: string,
11-
{
12-
headers = {},
13-
customFetch,
14-
}: {
15-
headers?: Record<string, string>
16-
customFetch?: Fetch
17-
} = {}
18-
) {
19-
this.url = url
20-
this.headers = headers
21-
this.fetch = resolveFetch(customFetch)
22-
}
23-
24-
/**
25-
* Updates the authorization header
26-
* @params token - the new jwt token sent in the authorisation header
27-
*/
28-
setAuth(token: string) {
29-
this.headers.Authorization = `Bearer ${token}`
30-
}
31-
32-
/**
33-
* Invokes a function
34-
* @param functionName - the name of the function to invoke
35-
*/
36-
async invoke<T = any>(
37-
functionName: string,
38-
invokeOptions?: FunctionInvokeOptions
39-
): Promise<{ data: T; error: null } | { data: null; error: Error }> {
40-
try {
41-
const { headers, body } = invokeOptions ?? {}
42-
const response = await this.fetch(`${this.url}/${functionName}`, {
43-
method: 'POST',
44-
headers: Object.assign({}, this.headers, headers),
45-
body,
46-
})
47-
48-
const isRelayError = response.headers.get('x-relay-error')
49-
if (isRelayError && isRelayError === 'true') {
50-
return { data: null, error: new Error(await response.text()) }
51-
}
52-
53-
let data
54-
const { responseType } = invokeOptions ?? {}
55-
if (!responseType || responseType === 'json') {
56-
data = await response.json()
57-
} else if (responseType === 'arrayBuffer') {
58-
data = await response.arrayBuffer()
59-
} else if (responseType === 'blob') {
60-
data = await response.blob()
61-
} else {
62-
data = await response.text()
63-
}
64-
65-
return { data, error: null }
66-
} catch (error: any) {
67-
return { data: null, error }
68-
}
69-
}
70-
}
1+
export { FunctionsClient } from './FunctionsClient'
2+
export {
3+
FunctionsError,
4+
FunctionsFetchError,
5+
FunctionsHttpError,
6+
FunctionsRelayError,
7+
FunctionsResponse,
8+
} from './types'

src/types.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
11
export type Fetch = typeof fetch
22

3-
export enum ResponseType {
4-
json,
5-
text,
6-
arrayBuffer,
7-
blob,
3+
/**
4+
* Response format
5+
*
6+
*/
7+
interface FunctionsResponseSuccess {
8+
data: any
9+
error: null
10+
}
11+
interface FunctionsResponseFailure {
12+
data: null
13+
error: any
14+
}
15+
export type FunctionsResponse = FunctionsResponseSuccess | FunctionsResponseFailure
16+
17+
export class FunctionsError extends Error {
18+
context: any
19+
constructor(message: string, name = 'FunctionsError', context?: any) {
20+
super(message)
21+
super.name = name
22+
this.context = context
23+
}
24+
}
25+
26+
export class FunctionsFetchError extends FunctionsError {
27+
constructor(context: any) {
28+
super('Failed to perform request to Edge Function', 'FunctionsFetchError', context)
29+
}
30+
}
31+
32+
export class FunctionsRelayError extends FunctionsError {
33+
constructor(context: any) {
34+
super('Relay error communicating with deno backend', 'FunctionsRelayError', context)
35+
}
836
}
937

10-
export type FunctionInvokeOptions = {
11-
/** object representing the headers to send with the request */
12-
headers?: { [key: string]: string }
13-
/** the body of the request */
14-
body?: Blob | BufferSource | FormData | URLSearchParams | ReadableStream<Uint8Array> | string
15-
/** how the response should be parsed. The default is `json` */
16-
responseType?: keyof typeof ResponseType
38+
export class FunctionsHttpError extends FunctionsError {
39+
constructor(context: any) {
40+
super('Edge Function returned a non-2xx status code', 'FunctionsHttpError', context)
41+
}
1742
}

0 commit comments

Comments
 (0)