Skip to content

Commit

Permalink
Add custom domain support to callable functions (#3825)
Browse files Browse the repository at this point in the history
  • Loading branch information
samtstern committed Sep 25, 2020
1 parent 3759005 commit a6af7c2
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/poor-eagles-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/functions': minor

This comment has been minimized.

Copy link
@Feiyang1

Feiyang1 Sep 26, 2020

Member

@samtstern Can you amend it with 'firebase': minor? It's needed to bump minor instead of patch in firebase.

This comment has been minimized.

Copy link
@samtstern

samtstern Sep 28, 2020

Author Contributor

Huh the changeset tool did not give me that option. I'll do a new changeset I guess?

---

Allow setting a custom domain for callable Cloud Functions.
8 changes: 5 additions & 3 deletions packages-exp/functions-exp/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,22 @@ 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(
app,
FUNCTIONS_TYPE
);
const functionsInstance = functionsProvider.getImmediate({
identifier: region
identifier: regionOrCustomDomain
});
return functionsInstance;
}
Expand Down
4 changes: 2 additions & 2 deletions packages-exp/functions-exp/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -42,7 +42,7 @@ export function registerFunctions(fetchImpl: typeof fetch): void {
app,
authProvider,
messagingProvider,
region,
regionOrCustomDomain,
fetchImpl
);
};
Expand Down
14 changes: 14 additions & 0 deletions packages-exp/functions-exp/src/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,19 @@ 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'
);
});
});
});
24 changes: 20 additions & 4 deletions packages-exp/functions-exp/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export class FunctionsService implements _FirebaseService {
emulatorOrigin: string | null = null;
cancelAllRequests: Promise<void>;
deleteService!: () => Promise<void>;
region: string;
customDomain: string | null;

/**
* Creates a new Functions service for the given app.
Expand All @@ -84,7 +86,7 @@ export class FunctionsService implements _FirebaseService {
readonly app: FirebaseApp,
authProvider: Provider<FirebaseAuthInternalName>,
messagingProvider: Provider<FirebaseMessagingName>,
readonly region: string = DEFAULT_REGION,
regionOrCustomDomain: string = DEFAULT_REGION,
readonly fetchImpl: typeof fetch
) {
this.contextProvider = new ContextProvider(authProvider, messagingProvider);
Expand All @@ -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 = DEFAULT_REGION;
} catch (e) {
this.customDomain = null;
this.region = regionOrCustomDomain;
}
}

_delete(): Promise<void> {
Expand All @@ -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}`;
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages-exp/functions-types-exp/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ declare module '@firebase/app-types' {
};
}
interface FirebaseApp {
functions?(region?: string): types.FirebaseFunctions;
functions?(regionOrCustomDomain?: string): types.FirebaseFunctions;
}
}
30 changes: 24 additions & 6 deletions packages/functions/src/api/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,21 @@ export class Service implements FirebaseFunctions, FirebaseService {
private emulatorOrigin: string | null = null;
private cancelAllRequests: Promise<void>;
private deleteService!: () => void;
private region: string;
private customDomain: string | 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<FirebaseAuthInternalName>,
messagingProvider: Provider<FirebaseMessagingName>,
private region_: string = 'us-central1',
regionOrCustomDomain_: string = 'us-central1',
readonly fetchImpl: typeof fetch
) {
this.contextProvider = new ContextProvider(authProvider, messagingProvider);
Expand All @@ -106,6 +110,16 @@ 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.customDomain = null;
this.region = regionOrCustomDomain_;
}
}

get app(): FirebaseApp {
Expand All @@ -124,12 +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}`;
}
return `https://${region}-${projectId}.cloudfunctions.net/${name}`;

if (this.customDomain !== null) {
return `${this.customDomain}/${name}`;
}

return `https://${this.region}-${projectId}.cloudfunctions.net/${name}`;
}

/**
Expand Down
13 changes: 11 additions & 2 deletions packages/functions/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,23 @@ 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)
Expand Down
20 changes: 17 additions & 3 deletions packages/functions/test/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,33 @@ describe('Firebase Functions > Service', () => {
});
});

describe('custom region constructor', () => {
describe('custom region/domain constructor', () => {
const app: any = {
options: {
projectId: 'my-project'
}
};
const service = createTestService(app, 'my-region');

it('has valid urls', () => {
it('can use custom region', () => {
const service = createTestService(app, 'my-region');
assert.equal(
service._url('foo'),
'https://my-region-my-project.cloudfunctions.net/foo'
);
});

it('can use custom domain', () => {
const 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');
service.useFunctionsEmulator('http://localhost:5005');
assert.equal(
service._url('foo'),
'http://localhost:5005/my-project/us-central1/foo'
);
});
});
});
4 changes: 2 additions & 2 deletions packages/functions/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function makeFakeApp(options: FirebaseOptions = {}): FirebaseApp {

export function createTestService(
app: FirebaseApp,
region?: string,
regionOrCustomDomain?: string,
authProvider = new Provider<FirebaseAuthInternalName>(
'auth-internal',
new ComponentContainer('test')
Expand All @@ -59,7 +59,7 @@ export function createTestService(
app,
authProvider,
messagingProvider,
region,
regionOrCustomDomain,
fetchImpl
);
const useEmulator = !!process.env.FIREBASE_FUNCTIONS_EMULATOR_ORIGIN;
Expand Down

0 comments on commit a6af7c2

Please sign in to comment.