From 3ee0f22aeaac42f569f7dfd091fa8fca074d50aa Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 22 Sep 2020 15:42:03 +0100 Subject: [PATCH 1/5] Add custom domain support to callable functions --- packages/functions/index.ts | 2 +- packages/functions/src/api/service.ts | 26 +++++++++++++++++++++---- packages/functions/src/config.ts | 4 ++-- packages/functions/test/service.test.ts | 25 ++++++++++++++++++++++++ packages/functions/test/utils.ts | 4 ++-- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/packages/functions/index.ts b/packages/functions/index.ts index 6a797b92029..1996574328d 100644 --- a/packages/functions/index.ts +++ b/packages/functions/index.ts @@ -32,6 +32,6 @@ declare module '@firebase/app-types' { }; } interface FirebaseApp { - functions?(region?: string): types.FirebaseFunctions; + functions?(regionOrCustomDomain?: string): types.FirebaseFunctions; } } diff --git a/packages/functions/src/api/service.ts b/packages/functions/src/api/service.ts index 601f365344b..c59aacce0fd 100644 --- a/packages/functions/src/api/service.ts +++ b/packages/functions/src/api/service.ts @@ -86,17 +86,21 @@ export class Service implements FirebaseFunctions, FirebaseService { private emulatorOrigin: string | null = null; private cancelAllRequests: Promise; private deleteService!: () => void; + private region: string; + private customDomain: string | null = null; /** - * Creates a new Functions service for the given app and (optional) region. + * Creates a new Functions service for the given app and (optional) region or custom domain. * @param app_ The FirebaseApp to use. - * @param region_ The region to call functions in. + * @param regionOrCustomDomain_ one of: + * a) A region to call functions from, such as us-central1 + * b) A custom domain to use as a functions prefix, such as https://mydomain.com */ constructor( private app_: FirebaseApp, authProvider: Provider, messagingProvider: Provider, - private region_: string = 'us-central1', + regionOrCustomDomain_: string = 'us-central1', readonly fetchImpl: typeof fetch ) { this.contextProvider = new ContextProvider(authProvider, messagingProvider); @@ -106,6 +110,15 @@ export class Service implements FirebaseFunctions, FirebaseService { return resolve(); }; }); + + // Resolve the region or custom domain overload by attempting to parse it. + try { + const url = new URL(regionOrCustomDomain_); + this.customDomain = url.origin; + this.region = 'us-central1'; + } catch (e) { + this.region = regionOrCustomDomain_; + } } get app(): FirebaseApp { @@ -124,11 +137,16 @@ export class Service implements FirebaseFunctions, FirebaseService { */ _url(name: string): string { const projectId = this.app_.options.projectId; - const region = this.region_; + const region = this.region; if (this.emulatorOrigin !== null) { const origin = this.emulatorOrigin; return `${origin}/${projectId}/${region}/${name}`; } + + if (this.customDomain !== null) { + return `${this.customDomain}/${name}`; + } + return `https://${region}-${projectId}.cloudfunctions.net/${name}`; } diff --git a/packages/functions/src/config.ts b/packages/functions/src/config.ts index c1cfdef7dac..dd5f6ae88b6 100644 --- a/packages/functions/src/config.ts +++ b/packages/functions/src/config.ts @@ -37,14 +37,14 @@ export function registerFunctions( Functions: Service }; - function factory(container: ComponentContainer, region?: string): Service { + function factory(container: ComponentContainer, regionOrCustomDomain?: string): Service { // Dependencies const app = container.getProvider('app').getImmediate(); const authProvider = container.getProvider('auth-internal'); const messagingProvider = container.getProvider('messaging'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new Service(app, authProvider, messagingProvider, region, fetchImpl); + return new Service(app, authProvider, messagingProvider, regionOrCustomDomain, fetchImpl); } instance.INTERNAL.registerComponent( new Component(FUNCTIONS_TYPE, factory, ComponentType.PUBLIC) diff --git a/packages/functions/test/service.test.ts b/packages/functions/test/service.test.ts index 197d8c6f741..93224a3440f 100644 --- a/packages/functions/test/service.test.ts +++ b/packages/functions/test/service.test.ts @@ -40,6 +40,31 @@ describe('Firebase Functions > Service', () => { 'http://localhost:5005/my-project/us-central1/foo' ); }); + + it('can use custom region', () => { + const customRegionService = createTestService(app, 'us-west2'); + assert.equal( + customRegionService._url('foo'), + 'https://us-west2-my-project.cloudfunctions.net/foo' + ); + }); + + it('can use custom domain', () => { + const customDomainService = createTestService(app, 'https://mydomain.com'); + assert.equal( + customDomainService._url('foo'), + 'https://mydomain.com/foo' + ); + }); + + it('prefers emulator to custom domain', () => { + const customDomainService = createTestService(app, 'https://mydomain.com'); + customDomainService.useFunctionsEmulator('http://localhost:5005'); + assert.equal( + customDomainService._url('foo'), + 'http://localhost:5005/my-project/us-central1/foo' + ); + }); }); describe('custom region constructor', () => { diff --git a/packages/functions/test/utils.ts b/packages/functions/test/utils.ts index 56c6f52201f..4ffdd8c868a 100644 --- a/packages/functions/test/utils.ts +++ b/packages/functions/test/utils.ts @@ -43,7 +43,7 @@ export function makeFakeApp(options: FirebaseOptions = {}): FirebaseApp { export function createTestService( app: FirebaseApp, - region?: string, + regionOrCustomDomain?: string, authProvider = new Provider( 'auth-internal', new ComponentContainer('test') @@ -59,7 +59,7 @@ export function createTestService( app, authProvider, messagingProvider, - region, + regionOrCustomDomain, fetchImpl ); const useEmulator = !!process.env.FIREBASE_FUNCTIONS_EMULATOR_ORIGIN; From 75b9e4ef603d0fc99952d1eafd028ff9ce2dddce Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 24 Sep 2020 13:00:03 +0100 Subject: [PATCH 2/5] Implement functions-exp --- packages-exp/functions-exp/src/api.ts | 8 ++-- packages-exp/functions-exp/src/config.ts | 4 +- .../functions-exp/src/service.test.ts | 17 ++++++++ packages-exp/functions-exp/src/service.ts | 24 +++++++++-- packages-exp/functions-types-exp/index.d.ts | 6 +++ packages/functions/src/api/service.ts | 8 ++-- packages/functions/test/service.test.ts | 40 ++++++++----------- 7 files changed, 70 insertions(+), 37 deletions(-) diff --git a/packages-exp/functions-exp/src/api.ts b/packages-exp/functions-exp/src/api.ts index 01d786f7737..3d7ff92f740 100644 --- a/packages-exp/functions-exp/src/api.ts +++ b/packages-exp/functions-exp/src/api.ts @@ -35,12 +35,14 @@ import { /** * Returns a Functions instance for the given app. * @param app - The FirebaseApp to use. - * @param region - The region the callable functions are located in. + * @param regionOrCustomDomain - one of: + * a) The region the callable functions are located in (ex: us-central1) + * b) A custom domain hosting the callable functions (ex: https://mydomain.com) * @public */ export function getFunctions( app: FirebaseApp, - region: string = DEFAULT_REGION + regionOrCustomDomain: string = DEFAULT_REGION ): Functions { // Dependencies const functionsProvider: Provider<'functions'> = _getProvider( @@ -48,7 +50,7 @@ export function getFunctions( FUNCTIONS_TYPE ); const functionsInstance = functionsProvider.getImmediate({ - identifier: region + identifier: regionOrCustomDomain }); return functionsInstance; } diff --git a/packages-exp/functions-exp/src/config.ts b/packages-exp/functions-exp/src/config.ts index 021235d6483..9279835b2c7 100644 --- a/packages-exp/functions-exp/src/config.ts +++ b/packages-exp/functions-exp/src/config.ts @@ -30,7 +30,7 @@ export const DEFAULT_REGION = 'us-central1'; export function registerFunctions(fetchImpl: typeof fetch): void { const factory: InstanceFactory<'functions'> = ( container: ComponentContainer, - region?: string + regionOrCustomDomain?: string ) => { // Dependencies const app = container.getProvider('app-exp').getImmediate(); @@ -42,7 +42,7 @@ export function registerFunctions(fetchImpl: typeof fetch): void { app, authProvider, messagingProvider, - region, + regionOrCustomDomain, fetchImpl ); }; diff --git a/packages-exp/functions-exp/src/service.test.ts b/packages-exp/functions-exp/src/service.test.ts index 79a4580976a..42b8e9eeb67 100644 --- a/packages-exp/functions-exp/src/service.test.ts +++ b/packages-exp/functions-exp/src/service.test.ts @@ -64,5 +64,22 @@ describe('Firebase Functions > Service', () => { 'http://localhost:5005/my-project/my-region/foo' ); }); + + it('correctly sets custom domain', () => { + service = createTestService(app, 'https://mydomain.com'); + assert.equal( + service._url('foo'), + 'https://mydomain.com/foo' + ); + }); + + it('prefers emulator to custom domain', () => { + const service = createTestService(app, 'https://mydomain.com'); + useFunctionsEmulator(service, 'http://localhost:5005'); + assert.equal( + service._url('foo'), + 'http://localhost:5005/my-project/us-central1/foo' + ); + }); }); }); diff --git a/packages-exp/functions-exp/src/service.ts b/packages-exp/functions-exp/src/service.ts index f51b838a0e0..e5c8c059a9b 100644 --- a/packages-exp/functions-exp/src/service.ts +++ b/packages-exp/functions-exp/src/service.ts @@ -75,6 +75,8 @@ export class FunctionsService implements _FirebaseService { emulatorOrigin: string | null = null; cancelAllRequests: Promise; deleteService!: () => Promise; + region: string; + customDomain: string | null; /** * Creates a new Functions service for the given app. @@ -84,7 +86,7 @@ export class FunctionsService implements _FirebaseService { readonly app: FirebaseApp, authProvider: Provider, messagingProvider: Provider, - readonly region: string = DEFAULT_REGION, + regionOrCustomDomain: string = DEFAULT_REGION, readonly fetchImpl: typeof fetch ) { this.contextProvider = new ContextProvider(authProvider, messagingProvider); @@ -94,6 +96,16 @@ export class FunctionsService implements _FirebaseService { return Promise.resolve(resolve()); }; }); + + // Resolve the region or custom domain overload by attempting to parse it. + try { + const url = new URL(regionOrCustomDomain); + this.customDomain = url.origin; + this.region = 'us-central1'; + } catch (e) { + this.customDomain = null; + this.region = DEFAULT_REGION; + } } _delete(): Promise { @@ -107,12 +119,16 @@ export class FunctionsService implements _FirebaseService { */ _url(name: string): string { const projectId = this.app.options.projectId; - const region = this.region; if (this.emulatorOrigin !== null) { const origin = this.emulatorOrigin; - return `${origin}/${projectId}/${region}/${name}`; + return `${origin}/${projectId}/${this.region}/${name}`; } - return `https://${region}-${projectId}.cloudfunctions.net/${name}`; + + if (this.customDomain !== null) { + return `${this.customDomain}/${name}`; + } + + return `https://${this.region}-${projectId}.cloudfunctions.net/${name}`; } } diff --git a/packages-exp/functions-types-exp/index.d.ts b/packages-exp/functions-types-exp/index.d.ts index e528fef46b7..9b529870825 100644 --- a/packages-exp/functions-types-exp/index.d.ts +++ b/packages-exp/functions-types-exp/index.d.ts @@ -54,6 +54,12 @@ export interface Functions { * Default is `us-central-1`. */ region: string; + + /** + * A custom domain hosting the callable Cloud Functions. + * ex: https://mydomain.com + */ + customDomain: string | null; } /** diff --git a/packages/functions/src/api/service.ts b/packages/functions/src/api/service.ts index c59aacce0fd..44b6a0cce30 100644 --- a/packages/functions/src/api/service.ts +++ b/packages/functions/src/api/service.ts @@ -87,7 +87,7 @@ export class Service implements FirebaseFunctions, FirebaseService { private cancelAllRequests: Promise; private deleteService!: () => void; private region: string; - private customDomain: string | null = null; + private customDomain: string | null; /** * Creates a new Functions service for the given app and (optional) region or custom domain. @@ -117,6 +117,7 @@ export class Service implements FirebaseFunctions, FirebaseService { this.customDomain = url.origin; this.region = 'us-central1'; } catch (e) { + this.customDomain = null; this.region = regionOrCustomDomain_; } } @@ -137,17 +138,16 @@ export class Service implements FirebaseFunctions, FirebaseService { */ _url(name: string): string { const projectId = this.app_.options.projectId; - const region = this.region; if (this.emulatorOrigin !== null) { const origin = this.emulatorOrigin; - return `${origin}/${projectId}/${region}/${name}`; + return `${origin}/${projectId}/${this.region}/${name}`; } if (this.customDomain !== null) { return `${this.customDomain}/${name}`; } - return `https://${region}-${projectId}.cloudfunctions.net/${name}`; + return `https://${this.region}-${projectId}.cloudfunctions.net/${name}`; } /** diff --git a/packages/functions/test/service.test.ts b/packages/functions/test/service.test.ts index 93224a3440f..324edec6f16 100644 --- a/packages/functions/test/service.test.ts +++ b/packages/functions/test/service.test.ts @@ -40,45 +40,37 @@ describe('Firebase Functions > Service', () => { 'http://localhost:5005/my-project/us-central1/foo' ); }); + }); + + describe('custom region/domain constructor', () => { + const app: any = { + options: { + projectId: 'my-project' + } + }; it('can use custom region', () => { - const customRegionService = createTestService(app, 'us-west2'); + const service = createTestService(app, 'my-region'); assert.equal( - customRegionService._url('foo'), - 'https://us-west2-my-project.cloudfunctions.net/foo' + service._url('foo'), + 'https://my-region-my-project.cloudfunctions.net/foo' ); }); it('can use custom domain', () => { - const customDomainService = createTestService(app, 'https://mydomain.com'); + const service = createTestService(app, 'https://mydomain.com'); assert.equal( - customDomainService._url('foo'), + service._url('foo'), 'https://mydomain.com/foo' ); }); it('prefers emulator to custom domain', () => { - const customDomainService = createTestService(app, 'https://mydomain.com'); - customDomainService.useFunctionsEmulator('http://localhost:5005'); - assert.equal( - customDomainService._url('foo'), - 'http://localhost:5005/my-project/us-central1/foo' - ); - }); - }); - - describe('custom region constructor', () => { - const app: any = { - options: { - projectId: 'my-project' - } - }; - const service = createTestService(app, 'my-region'); - - it('has valid urls', () => { + const service = createTestService(app, 'https://mydomain.com'); + service.useFunctionsEmulator('http://localhost:5005'); assert.equal( service._url('foo'), - 'https://my-region-my-project.cloudfunctions.net/foo' + 'http://localhost:5005/my-project/us-central1/foo' ); }); }); From 4df34d973b27ac9fbb1e9c6ff09d9d9e206ac4e9 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 24 Sep 2020 13:01:10 +0100 Subject: [PATCH 3/5] Prettier --- packages-exp/functions-exp/src/service.test.ts | 5 +---- packages/functions/src/config.ts | 13 +++++++++++-- packages/functions/test/service.test.ts | 5 +---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages-exp/functions-exp/src/service.test.ts b/packages-exp/functions-exp/src/service.test.ts index 42b8e9eeb67..9f518d29284 100644 --- a/packages-exp/functions-exp/src/service.test.ts +++ b/packages-exp/functions-exp/src/service.test.ts @@ -67,10 +67,7 @@ describe('Firebase Functions > Service', () => { it('correctly sets custom domain', () => { service = createTestService(app, 'https://mydomain.com'); - assert.equal( - service._url('foo'), - 'https://mydomain.com/foo' - ); + assert.equal(service._url('foo'), 'https://mydomain.com/foo'); }); it('prefers emulator to custom domain', () => { diff --git a/packages/functions/src/config.ts b/packages/functions/src/config.ts index dd5f6ae88b6..df833a072ba 100644 --- a/packages/functions/src/config.ts +++ b/packages/functions/src/config.ts @@ -37,14 +37,23 @@ export function registerFunctions( Functions: Service }; - function factory(container: ComponentContainer, regionOrCustomDomain?: string): Service { + function factory( + container: ComponentContainer, + regionOrCustomDomain?: string + ): Service { // Dependencies const app = container.getProvider('app').getImmediate(); const authProvider = container.getProvider('auth-internal'); const messagingProvider = container.getProvider('messaging'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new Service(app, authProvider, messagingProvider, regionOrCustomDomain, fetchImpl); + return new Service( + app, + authProvider, + messagingProvider, + regionOrCustomDomain, + fetchImpl + ); } instance.INTERNAL.registerComponent( new Component(FUNCTIONS_TYPE, factory, ComponentType.PUBLIC) diff --git a/packages/functions/test/service.test.ts b/packages/functions/test/service.test.ts index 324edec6f16..d91d1b683b8 100644 --- a/packages/functions/test/service.test.ts +++ b/packages/functions/test/service.test.ts @@ -59,10 +59,7 @@ describe('Firebase Functions > Service', () => { it('can use custom domain', () => { const service = createTestService(app, 'https://mydomain.com'); - assert.equal( - service._url('foo'), - 'https://mydomain.com/foo' - ); + assert.equal(service._url('foo'), 'https://mydomain.com/foo'); }); it('prefers emulator to custom domain', () => { From f61ba5aa1f899c20ac5aabdcf9611d23ce93a058 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 24 Sep 2020 13:02:09 +0100 Subject: [PATCH 4/5] Add changeset --- .changeset/poor-eagles-think.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/poor-eagles-think.md diff --git a/.changeset/poor-eagles-think.md b/.changeset/poor-eagles-think.md new file mode 100644 index 00000000000..9035b59e803 --- /dev/null +++ b/.changeset/poor-eagles-think.md @@ -0,0 +1,5 @@ +--- +'@firebase/functions': minor +--- + +Allow setting a custom domain for callable Cloud Functions. From 05e6e0aa4508e943949890a1f9d04fc1d7d4eee4 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 25 Sep 2020 10:53:46 +0100 Subject: [PATCH 5/5] Code review --- packages-exp/functions-exp/src/service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages-exp/functions-exp/src/service.ts b/packages-exp/functions-exp/src/service.ts index e5c8c059a9b..30c557761a5 100644 --- a/packages-exp/functions-exp/src/service.ts +++ b/packages-exp/functions-exp/src/service.ts @@ -101,10 +101,10 @@ export class FunctionsService implements _FirebaseService { try { const url = new URL(regionOrCustomDomain); this.customDomain = url.origin; - this.region = 'us-central1'; + this.region = DEFAULT_REGION; } catch (e) { this.customDomain = null; - this.region = DEFAULT_REGION; + this.region = regionOrCustomDomain; } }