Skip to content

Commit

Permalink
feat: sdk: add setHeaders for setting global headers and make sure …
Browse files Browse the repository at this point in the history
…all client calls have a `headers` arg (#2697)

resolves #2696
  • Loading branch information
onehassan committed May 13, 2024
1 parent 68e0622 commit 304065a
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 30 deletions.
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
Loading

0 comments on commit 304065a

Please sign in to comment.