Skip to content

Add custom domain support to callable functions #3825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
---

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I make it private then it remains a private instance field, but I want it to just be a constructor arg that's not saved outside this scope. So I think it's correct as-is?

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