diff --git a/.changeset/new-bugs-think.md b/.changeset/new-bugs-think.md new file mode 100644 index 00000000000..5f663f436fe --- /dev/null +++ b/.changeset/new-bugs-think.md @@ -0,0 +1,6 @@ +--- +"@firebase/functions-compat": patch +"@firebase/functions": patch +--- + +Add httpsCallableFromURL diff --git a/common/api-review/functions.api.md b/common/api-review/functions.api.md index 36bacc8f443..4203be6645f 100644 --- a/common/api-review/functions.api.md +++ b/common/api-review/functions.api.md @@ -1,49 +1,52 @@ -## API Report File for "@firebase/functions" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { FirebaseApp } from '@firebase/app'; -import { FirebaseError } from '@firebase/util'; - -// @public -export function connectFunctionsEmulator(functionsInstance: Functions, host: string, port: number): void; - -// @public -export interface Functions { - app: FirebaseApp; - customDomain: string | null; - region: string; -} - -// @public -export interface FunctionsError extends FirebaseError { - readonly code: FunctionsErrorCode; - readonly details?: unknown; -} - -// @public -export type FunctionsErrorCode = 'ok' | 'cancelled' | 'unknown' | 'invalid-argument' | 'deadline-exceeded' | 'not-found' | 'already-exists' | 'permission-denied' | 'resource-exhausted' | 'failed-precondition' | 'aborted' | 'out-of-range' | 'unimplemented' | 'internal' | 'unavailable' | 'data-loss' | 'unauthenticated'; - -// @public -export function getFunctions(app?: FirebaseApp, regionOrCustomDomain?: string): Functions; - -// @public -export type HttpsCallable = (data?: RequestData | null) => Promise>; - -// @public -export function httpsCallable(functionsInstance: Functions, name: string, options?: HttpsCallableOptions): HttpsCallable; - -// @public -export interface HttpsCallableOptions { - timeout?: number; -} - -// @public -export interface HttpsCallableResult { - readonly data: ResponseData; -} - - -``` +## API Report File for "@firebase/functions" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { FirebaseApp } from '@firebase/app'; +import { FirebaseError } from '@firebase/util'; + +// @public +export function connectFunctionsEmulator(functionsInstance: Functions, host: string, port: number): void; + +// @public +export interface Functions { + app: FirebaseApp; + customDomain: string | null; + region: string; +} + +// @public +export interface FunctionsError extends FirebaseError { + readonly code: FunctionsErrorCode; + readonly details?: unknown; +} + +// @public +export type FunctionsErrorCode = 'ok' | 'cancelled' | 'unknown' | 'invalid-argument' | 'deadline-exceeded' | 'not-found' | 'already-exists' | 'permission-denied' | 'resource-exhausted' | 'failed-precondition' | 'aborted' | 'out-of-range' | 'unimplemented' | 'internal' | 'unavailable' | 'data-loss' | 'unauthenticated'; + +// @public +export function getFunctions(app?: FirebaseApp, regionOrCustomDomain?: string): Functions; + +// @public +export type HttpsCallable = (data?: RequestData | null) => Promise>; + +// @public +export function httpsCallable(functionsInstance: Functions, name: string, options?: HttpsCallableOptions): HttpsCallable; + +// @public +export function httpsCallableFromURL(functionsInstance: Functions, url: string, options?: HttpsCallableOptions): HttpsCallable; + +// @public +export interface HttpsCallableOptions { + timeout?: number; +} + +// @public +export interface HttpsCallableResult { + readonly data: ResponseData; +} + + +``` diff --git a/e2e/sample-apps/modular.js b/e2e/sample-apps/modular.js index b5d1c060f93..04c6d95af21 100644 --- a/e2e/sample-apps/modular.js +++ b/e2e/sample-apps/modular.js @@ -119,10 +119,27 @@ async function authLogout(app) { async function callFunctions(app) { console.log('[FUNCTIONS] start'); const functions = getFunctions(app); - const callTest = httpsCallable(functions, 'callTest'); + let callTest = httpsCallable(functions, 'callTest'); try { const result = await callTest({ data: 'blah' }); - console.log('[FUNCTIONS] result:', result.data); + console.log('[FUNCTIONS] result (by name):', result.data); + } catch (e) { + if (e.message.includes('Unauthenticated')) { + console.warn( + 'Functions blocked by App Check. ' + + 'Activate app check with a live sitekey to allow Functions calls' + ); + } else { + throw e; + } + } + callTest = httpsCallableByUrl( + functions, + `https://us-central-${app.options.projectId}.cloudfunctions.net/callTest` + ); + try { + const result = await callTest({ data: 'blah' }); + console.log('[FUNCTIONS] result (by URL):', result.data); } catch (e) { if (e.message.includes('Unauthenticated')) { console.warn( diff --git a/e2e/tests/modular.test.ts b/e2e/tests/modular.test.ts index a7fec241653..e5864452d70 100644 --- a/e2e/tests/modular.test.ts +++ b/e2e/tests/modular.test.ts @@ -65,7 +65,12 @@ import { Firestore, initializeFirestore } from 'firebase/firestore'; -import { Functions, getFunctions, httpsCallable } from 'firebase/functions'; +import { + Functions, + getFunctions, + httpsCallable, + httpsCallableFromURL +} from 'firebase/functions'; import { getMessaging } from 'firebase/messaging'; import { FirebasePerformance, @@ -144,6 +149,15 @@ describe('MODULAR', () => { expect(result.data.word).to.equal('hellooo'); // This takes a while. Extend timeout past default (2000) }).timeout(5000); + it('httpsCallableFromURL()', async () => { + const callTest = httpsCallableFromURL<{ data: string }, { word: string }>( + functions, + `https://us-central1-${app.options.projectId}.cloudfunctions.net/callTest` + ); + const result = await callTest({ data: 'blah' }); + expect(result.data.word).to.equal('hellooo'); + // This takes a while. Extend timeout past default (2000) + }).timeout(5000); }); describe('STORAGE', async () => { diff --git a/packages/functions-compat/src/service.ts b/packages/functions-compat/src/service.ts index b7a5a7d67dc..e1a9a34e8ae 100644 --- a/packages/functions-compat/src/service.ts +++ b/packages/functions-compat/src/service.ts @@ -18,6 +18,7 @@ import { FirebaseFunctions, HttpsCallable } from '@firebase/functions-types'; import { httpsCallable as httpsCallableExp, + httpsCallableFromURL as httpsCallableFromURLExp, connectFunctionsEmulator as useFunctionsEmulatorExp, HttpsCallableOptions, Functions as FunctionsServiceExp @@ -47,6 +48,12 @@ export class FunctionsService implements FirebaseFunctions, _FirebaseService { httpsCallable(name: string, options?: HttpsCallableOptions): HttpsCallable { return httpsCallableExp(this._delegate, name, options); } + httpsCallableFromURL( + url: string, + options?: HttpsCallableOptions + ): HttpsCallable { + return httpsCallableFromURLExp(this._delegate, url, options); + } /** * Deprecated in pre-modularized repo, does not exist in modularized * functions package, need to convert to "host" and "port" args that diff --git a/packages/functions/src/api.ts b/packages/functions/src/api.ts index 008d541b059..52913263041 100644 --- a/packages/functions/src/api.ts +++ b/packages/functions/src/api.ts @@ -24,7 +24,8 @@ import { FunctionsService, DEFAULT_REGION, connectFunctionsEmulator as _connectFunctionsEmulator, - httpsCallable as _httpsCallable + httpsCallable as _httpsCallable, + httpsCallableFromURL as _httpsCallableFromURL } from './service'; import { getModularInstance } from '@firebase/util'; @@ -90,3 +91,23 @@ export function httpsCallable( options ); } + +/** + * Returns a reference to the callable HTTPS trigger with the specified url. + * @param url - The url of the trigger. + * @public + */ +export function httpsCallableFromURL< + RequestData = unknown, + ResponseData = unknown +>( + functionsInstance: Functions, + url: string, + options?: HttpsCallableOptions +): HttpsCallable { + return _httpsCallableFromURL( + getModularInstance(functionsInstance as FunctionsService), + url, + options + ); +} diff --git a/packages/functions/src/service.ts b/packages/functions/src/service.ts index db17b9a29e6..954954bd3fd 100644 --- a/packages/functions/src/service.ts +++ b/packages/functions/src/service.ts @@ -186,6 +186,21 @@ export function httpsCallable( }) as HttpsCallable; } +/** + * Returns a reference to the callable https trigger with the given url. + * @param url - The url of the trigger. + * @public + */ +export function httpsCallableFromURL( + functionsInstance: FunctionsService, + url: string, + options?: HttpsCallableOptions +): HttpsCallable { + return (data => { + return callAtURL(functionsInstance, url, data, options || {}); + }) as HttpsCallable; +} + /** * Does an HTTP POST and returns the completed response. * @param url The url to post to. @@ -235,14 +250,27 @@ async function postJSON( * @param name The name of the callable trigger. * @param data The data to pass as params to the function.s */ -async function call( +function call( functionsInstance: FunctionsService, name: string, data: unknown, options: HttpsCallableOptions ): Promise { const url = functionsInstance._url(name); + return callAtURL(functionsInstance, url, data, options); +} +/** + * Calls a callable function asynchronously and returns the result. + * @param url The url of the callable trigger. + * @param data The data to pass as params to the function.s + */ +async function callAtURL( + functionsInstance: FunctionsService, + url: string, + data: unknown, + options: HttpsCallableOptions +): Promise { // Encode any special types, such as dates, in the input data. data = encode(data); const body = { data };