diff --git a/package.json b/package.json index 33bb6ff..fa0e813 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sqlitecloud/drivers", - "version": "1.0.360", + "version": "1.0.361", "description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/src/packages/_functions/FunctionsClient.ts b/src/packages/_functions/FunctionsClient.ts index 0b0a738..e341953 100644 --- a/src/packages/_functions/FunctionsClient.ts +++ b/src/packages/_functions/FunctionsClient.ts @@ -10,8 +10,9 @@ import { Fetch, resolveFetch } from '../utils/fetch' * @param headers - The headers to pass to the function. */ interface FunctionInvokeOptions { - args: any[] + params: Record headers?: Record + apiKey?: string } /** @@ -27,13 +28,15 @@ export class FunctionsClient { constructor( connectionString: string, options: { - customFetch?: Fetch, + fetch?: Fetch, headers?: Record - } = {} + } = { + headers: {} + } ) { this.url = getAPIUrl(connectionString, FUNCTIONS_ROOT_PATH) - this.fetch = resolveFetch(options.customFetch) - this.headers = options.headers ? { ...DEFAULT_HEADERS, ...options.headers } : { ...DEFAULT_HEADERS } + this.fetch = resolveFetch(options.fetch) + this.headers = { ...DEFAULT_HEADERS, ...options.headers } } // TODO: check authorization and api key setup in Gateway setAuth(token: string) { @@ -43,56 +46,42 @@ export class FunctionsClient { async invoke(functionId: string, options: FunctionInvokeOptions) { let body; let _headers: Record = {} - if (options.args && + if (options.params && ((options.headers && !Object.prototype.hasOwnProperty.call(options.headers, 'Content-Type')) || !options.headers) ) { if ( - (typeof Blob !== 'undefined' && options.args instanceof Blob) || - options.args instanceof ArrayBuffer + (typeof Blob !== 'undefined' && options.params instanceof Blob) || + options.params instanceof ArrayBuffer ) { // will work for File as File inherits Blob // also works for ArrayBuffer as it is the same underlying structure as a Blob _headers['Content-Type'] = 'application/octet-stream' - body = options.args - } else if (typeof options.args === 'string') { + body = options.params + } else if (typeof options.params === 'string') { // plain string _headers['Content-Type'] = 'text/plain' - body = options.args - } else if (typeof FormData !== 'undefined' && options.args instanceof FormData) { + body = options.params + } else if (typeof FormData !== 'undefined' && options.params instanceof FormData) { _headers['Content-Type'] = 'multipart/form-data' - body = options.args + body = options.params } else { // default, assume this is JSON _headers['Content-Type'] = 'application/json' - body = JSON.stringify(options.args) + body = JSON.stringify(options.params) } } try { const response = await this.fetch(`${this.url}/${functionId}`, { method: 'POST', - body: JSON.stringify(options.args), + body, headers: { ..._headers, ...this.headers, ...options.headers } }) if (!response.ok) { throw new SQLiteCloudError(`Failed to invoke function: ${response.statusText}`) } - - let responseType = (response.headers.get('Content-Type') ?? 'text/plain').split(';')[0].trim() - let data: any - if (responseType === 'application/json') { - data = await response.json() - } else if (responseType === 'application/octet-stream') { - data = await response.blob() - } else if (responseType === 'text/event-stream') { - data = response - } else if (responseType === 'multipart/form-data') { - data = await response.formData() - } else { - data = await response.text() - } - return { ...data, error: null } + return { error: null, ...(await response.json()) } } catch (error) { return { data: null, error } } diff --git a/src/packages/test/functions.test.ts b/src/packages/test/functions.test.ts new file mode 100644 index 0000000..4c400da --- /dev/null +++ b/src/packages/test/functions.test.ts @@ -0,0 +1,44 @@ + +// Test functions client +// invoke + +import { CHINOOK_API_KEY, CHINOOK_DATABASE_URL } from "../../../test/shared" +import { FunctionsClient } from "../_functions/FunctionsClient" + +const TEST_SQL_FUNCTION_ID = 'test-1-sql' +const TEST_JS_FUNCTION_ID = 'test-1-js' + +const TEST_FUNCTION_ARG = { + filter: 'a', + limit: 10 +} + +const functions = new FunctionsClient(CHINOOK_DATABASE_URL) + +describe('FunctionsClient', () => { + it('should invoke a JS function', async () => { + + const { data, error } = await functions.invoke(TEST_JS_FUNCTION_ID, { + params: TEST_FUNCTION_ARG, + headers: { + 'Authorization': `Bearer ${CHINOOK_API_KEY}` + } + }) + expect(data.message).toBeDefined() + expect(data.result).toBeDefined() + expect(error).toBeNull() + }) + + it('should invoke a SQL function', async () => { + const { data, error } = await functions.invoke(TEST_SQL_FUNCTION_ID, { + params: TEST_FUNCTION_ARG, + headers: { + 'Authorization': `Bearer ${CHINOOK_API_KEY}` + } + }) + expect(data).toBeDefined() + expect(data.length > 0).toBeTruthy() + expect(error).toBeNull() + }) +}) + diff --git a/src/packages/utils/fetch.ts b/src/packages/utils/fetch.ts index 4a11340..9901f28 100644 --- a/src/packages/utils/fetch.ts +++ b/src/packages/utils/fetch.ts @@ -7,11 +7,11 @@ export const resolveFetch = (customFetch?: Fetch): Fetch => { if (customFetch) { _fetch = customFetch } else if (typeof fetch !== 'undefined') { - _fetch = nodeFetch as unknown as Fetch - } else { _fetch = fetch + } else { + _fetch = nodeFetch as unknown as Fetch } - return (...args: Parameters) => _fetch(...args) + return _fetch } export const resolveHeadersConstructor = () => { diff --git a/src/packages/weblite/WebliteClient.ts b/src/packages/weblite/WebliteClient.ts index b9ebc15..20b30dc 100644 --- a/src/packages/weblite/WebliteClient.ts +++ b/src/packages/weblite/WebliteClient.ts @@ -21,7 +21,7 @@ export class WebliteClient { ) { this.baseUrl = getAPIUrl(connectionString, 'weblite') this.fetch = options?.fetch || fetchWithAuth(connectionString) - this.headers = { ...options.headers } + this.headers = { ...DEFAULT_HEADERS, ...options.headers } this._defaultDatabase = getDefaultDatabase(connectionString) } @@ -120,7 +120,7 @@ export class WebliteClient { const filenamePath = encodeURIComponent(filename) const url = `${this.baseUrl}/${filenamePath}` try { - const response = await this.fetch(url, { method: 'GET', headers: { ...this.headers } }) + const response = await this.fetch(url, { method: 'GET', headers: this.headers }) if (!response.ok) { throw new SQLiteCloudError(`Failed to download database: ${response.statusText}`) } @@ -141,7 +141,7 @@ export class WebliteClient { url, { method: 'DELETE', - headers: { ...this.headers } + headers: this.headers } ) if (!response.ok) { @@ -156,7 +156,7 @@ export class WebliteClient { async listDatabases() { const url = `${this.baseUrl}/databases` try { - const response = await this.fetch(url, { method: 'GET', headers: { ...this.headers } }) + const response = await this.fetch(url, { method: 'GET', headers: this.headers }) if (!response.ok) { throw new SQLiteCloudError(`Failed to list databases: ${response.statusText}`) }