Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sdk: add setHeaders for setting global headers and make sure all client calls have a headers arg #2697

Merged
merged 8 commits into from
May 13, 2024
7 changes: 7 additions & 0 deletions .changeset/spicy-trains-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@nhost/hasura-storage-js': minor
'@nhost/graphql-js': minor
'@nhost/nhost-js': minor
---

feat: add `setHeaders` method enabling global configuration of storage, graphql, and functions client headers, alongside added support for passing specific headers with individual calls
61 changes: 59 additions & 2 deletions packages/graphql-js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class NhostGraphqlClient {
readonly _url: string
private accessToken: string | null
private adminSecret?: string
private headers: Record<string, string> = {}

constructor(params: NhostGraphqlConstructorParams) {
const { url, adminSecret } = params
Expand Down Expand Up @@ -101,7 +102,7 @@ export class NhostGraphqlClient {
const [variables, config] = variablesAndRequestHeaders
const requestOptions = parseRequestArgs(documentOrOptions, variables, config)

const { headers, ...otherOptions } = config || {}
const { headers: extraHeaders, ...otherOptions } = config || {}
const { query, operationName } = resolveRequestDocument(requestOptions.document)

if (typeof process !== 'undefined' && !process.env.TEST_MODE) {
Expand All @@ -120,7 +121,8 @@ export class NhostGraphqlClient {
headers: {
'Content-Type': 'application/json',
...this.generateAccessTokenHeaders(),
...headers
...this.headers, // graphql client headers to be sent with all `request` calls
...extraHeaders // extra headers to be sent with a specific call
},
...otherOptions
})
Expand Down Expand Up @@ -228,6 +230,61 @@ export class NhostGraphqlClient {
this.accessToken = accessToken
}

/**
* Use `nhost.graphql.getHeaders` to get the global headers sent with all graphql requests
*
* @example
* ```ts
* nhost.graphql.getHeaders()
* ```
*
* @docs https://docs.nhost.io/reference/javascript/graphql/get-headers
*/
getHeaders(): Record<string, string> {
return this.headers
}

/**
* Use `nhost.graphql.setHeaders` to set global headers to be sent in all subsequent graphql requests
*
* @example
* ```ts
* nhost.graphql.setHeaders({
* 'x-hasura-role': 'admin'
* })
* ```
*
* @docs https://docs.nhost.io/reference/javascript/graphql/set-headers
*/
setHeaders(headers?: Record<string, string>) {
if (!headers) {
return
}

this.headers = {
...this.headers,
...headers
}
}

/**
* Use `nhost.graphql.unsetHeaders` to remove global headers sent with all requests, except for the role header to preserve
* the role set by 'setRole' method.
*
* @example
* ```ts
* nhost.graphql.unsetHeaders()
* ```
*
* @docs https://docs.nhost.io/reference/javascript/graphql/unset-headers
*/
unsetHeaders() {
const userRole = this.headers['x-hasura-role']

// preserve the user role header to avoid invalidating preceding 'setRole' call.
this.headers = userRole ? { 'x-hasura-role': userRole } : {}
}

private generateAccessTokenHeaders(): NhostGraphqlRequestConfig['headers'] {
if (this.adminSecret) {
return {
Expand Down
92 changes: 78 additions & 14 deletions packages/hasura-storage-js/src/hasura-storage-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,25 @@ export class HasuraStorageApi {
private url: string
private accessToken?: string
private adminSecret?: string
private headers: Record<string, string> = {}

constructor({ url }: { url: string }) {
this.url = url
}

async uploadFormData({
formData,
headers,
bucketId
bucketId,
headers: extraHeaders
}: StorageUploadFormDataParams): Promise<StorageUploadFormDataResponse> {
const { error, fileMetadata } = await fetchUpload(this.url, formData, {
accessToken: this.accessToken,
adminSecret: this.adminSecret,
bucketId,
headers
headers: {
...this.headers, // global nhost storage client headers to be sent with all `uploadFormData` calls
...extraHeaders // extra headers to be sent with a specific call
},
accessToken: this.accessToken,
adminSecret: this.adminSecret
})

if (error) {
Expand All @@ -67,7 +71,8 @@ export class HasuraStorageApi {
file,
bucketId,
id,
name
name,
headers: extraHeaders
}: StorageUploadFileParams): Promise<StorageUploadFileResponse> {
const formData = typeof window === 'undefined' ? new LegacyFormData() : new FormData()

Expand All @@ -79,7 +84,11 @@ export class HasuraStorageApi {
adminSecret: this.adminSecret,
bucketId,
fileId: id,
name
name,
headers: {
...this.headers, // global nhost storage client headers to be sent with all `uploadFile` calls
...extraHeaders // extra headers to be sent with a specific call
}
})

if (error) {
Expand All @@ -98,7 +107,7 @@ export class HasuraStorageApi {

async downloadFile(params: StorageDownloadFileParams): Promise<StorageDownloadFileResponse> {
try {
const { fileId, headers: customHeaders = {}, ...imageTransformationParams } = params
const { fileId, headers: extraHeaders, ...imageTransformationParams } = params

const urlWithParams = appendImageTransformationParameters(
`${this.url}/files/${fileId}`,
Expand All @@ -107,7 +116,11 @@ export class HasuraStorageApi {

const response = await fetch(urlWithParams, {
method: 'GET',
headers: {...this.generateAuthHeaders(), ...customHeaders}
headers: {
...this.generateAuthHeaders(),
...this.headers, // global nhost storage client headers to be sent with all `downloadFile` calls
...extraHeaders // extra headers to be sent with a specific call
}
})

if (!response.ok) {
Expand All @@ -124,11 +137,15 @@ export class HasuraStorageApi {

async getPresignedUrl(params: ApiGetPresignedUrlParams): Promise<ApiGetPresignedUrlResponse> {
try {
const { fileId } = params
const { fileId, headers: extraHeaders } = params

const response = await fetch(`${this.url}/files/${fileId}/presignedurl`, {
method: 'GET',
headers: this.generateAuthHeaders()
headers: {
...this.generateAuthHeaders(),
...this.headers, // global nhost storage client headers to be sent with all `getPresignedUrl` calls
...extraHeaders // extra headers to be sent with a specific call
}
})
if (!response.ok) {
throw new Error(await response.text())
Expand All @@ -142,10 +159,14 @@ export class HasuraStorageApi {

async delete(params: ApiDeleteParams): Promise<ApiDeleteResponse> {
try {
const { fileId } = params
const { fileId, headers: extraHeaders } = params
const response = await fetch(`${this.url}/files/${fileId}`, {
method: 'DELETE',
headers: this.generateAuthHeaders()
headers: {
...this.generateAuthHeaders(),
...this.headers, // global nhost storage client headers to be sent with all `delete` calls
...extraHeaders // extra headers to be sent with a specific call
}
})
if (!response.ok) {
throw new Error(await response.text())
Expand Down Expand Up @@ -180,6 +201,49 @@ export class HasuraStorageApi {
return this
}

/**
* Get global headers sent with all requests.
*
* @returns Record<string, string>
*/
getHeaders(): Record<string, string> {
return this.headers
}

/**
* Set global headers to be sent with all requests.
*
* @param headers a key value pair headers object
* @returns Hasura Storage API instance
*/
setHeaders(headers?: Record<string, string>): HasuraStorageApi {
if (!headers) {
return this
}

this.headers = {
...this.headers,
...headers
}

return this
}

/**
* Remove global headers sent with all requests, except for the role header to preserve
* the role set by 'setRole' method.
*
* @returns {HasuraStorageApi} - Hasura Storage API instance.
*/
unsetHeaders(): HasuraStorageApi {
const userRole = this.headers['x-hasura-role']

// preserve the user role header to avoid invalidating preceding 'setRole' call.
this.headers = userRole ? { 'x-hasura-role': userRole } : {}

return this
}

private generateAuthHeaders(): HeadersInit | undefined {
if (!this.adminSecret && !this.accessToken) {
return undefined
Expand Down
51 changes: 51 additions & 0 deletions packages/hasura-storage-js/src/hasura-storage-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,55 @@ export class HasuraStorageClient {

return this
}

/**
* Use `nhost.storage.getHeaders` to get global headers sent with all storage requests.
*
* @example
* ```ts
* nhost.storage.getHeaders()
* ```
*
* @docs https://docs.nhost.io/reference/javascript/storage/get-headers
*/
getHeaders(): Record<string, string> {
return this.getHeaders()
}

/**
* Use `nhost.storage.setHeaders` to set global headers to be sent for all subsequent storage requests.
*
* @example
* ```ts
* nhost.storage.setHeaders({
* 'x-hasura-role': 'admin'
* })
* ```
*
* @param headers key value headers object
*
* @docs https://docs.nhost.io/reference/javascript/storage/set-headers
*/
setHeaders(headers?: Record<string, string>): HasuraStorageClient {
this.api.setHeaders(headers)

return this
}

/**
* Use `nhost.storage.unsetHeaders` to remove the global headers sent for all subsequent storage requests.
*
* @example
* ```ts
* nhost.storage.unsetHeaders()
* ```
*
* @param headers key value headers object
*
* @docs https://docs.nhost.io/reference/javascript/storage/unset-headers
*/
unsetHeaders(): HasuraStorageClient {
this.api.unsetHeaders()
return this
}
}
21 changes: 11 additions & 10 deletions packages/hasura-storage-js/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,22 @@ export interface FileUploadConfig {
adminSecret?: string
}

export interface StorageHeadersParam {
headers?: Record<string, string>
}

// works only in browser. Used for for hooks
export interface StorageUploadFileParams {
export interface StorageUploadFileParams extends StorageHeadersParam {
file: File
id?: string
name?: string
bucketId?: string
}

// works in browser and server
export interface StorageUploadFormDataParams {
export interface StorageUploadFormDataParams extends StorageHeadersParam {
formData: FormData | LegacyFormData
bucketId?: string
headers?: Record<string, string>
}

// works in browser and server
Expand All @@ -53,12 +56,10 @@ export type StorageUploadFormDataResponse =

export type StorageUploadResponse = StorageUploadFileResponse | StorageUploadFormDataResponse

export interface StorageDownloadFileParams extends StorageImageTransformationParams {
export interface StorageDownloadFileParams
extends StorageImageTransformationParams,
StorageHeadersParam {
fileId: string
/**
* Optional headers to be sent with the request
*/
headers?: Record<string, string>
}

export type StorageDownloadFileResponse = { file: Blob; error: null } | { file: null; error: Error }
Expand Down Expand Up @@ -111,15 +112,15 @@ export interface FileResponse {

// TODO not implemented yet in hasura-storage
// export interface ApiGetPresignedUrlParams extends StorageImageTransformationParams {
export interface ApiGetPresignedUrlParams {
export interface ApiGetPresignedUrlParams extends StorageHeadersParam {
fileId: string
}

export type ApiGetPresignedUrlResponse =
| { presignedUrl: { url: string; expiration: number }; error: null }
| { presignedUrl: null; error: Error }

export interface ApiDeleteParams {
export interface ApiDeleteParams extends StorageHeadersParam {
fileId: string
}

Expand Down