diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..7f3b10154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Adds support for multi-region RTDB. diff --git a/spec/providers/database.spec.ts b/spec/providers/database.spec.ts index 25293ac11..469dc4551 100644 --- a/spec/providers/database.spec.ts +++ b/spec/providers/database.spec.ts @@ -424,30 +424,72 @@ describe('Database Functions', () => { }); }); - describe('resourceToInstanceAndPath', () => { - it('should return the correct instance and path strings', () => { - const [instance, path] = database.resourceToInstanceAndPath( - 'projects/_/instances/foo/refs/bar' - ); + describe('extractInstanceAndPath', () => { + it('should return the correct us-central prod instance and path strings', () => { + const [instance, path] = database.extractInstanceAndPath({ + context: { + resource: { + name: 'projects/_/instances/foo/refs/bar', + }, + }, + }); expect(instance).to.equal('https://foo.firebaseio.com'); expect(path).to.equal('/bar'); }); + it('should return the correct staging instance and path strings', () => { + const [instance, path] = database.extractInstanceAndPath({ + context: { + resource: { + name: 'projects/_/instances/foo/refs/bar', + }, + domain: 'firebaseio-staging.com', + }, + }); + expect(instance).to.equal('https://foo.firebaseio-staging.com'); + expect(path).to.equal('/bar'); + }); + + it('should return the correct multi-region instance and path strings', () => { + const [instance, path] = database.extractInstanceAndPath({ + context: { + resource: { + name: 'projects/_/instances/foo/refs/bar', + }, + domain: 'euw1.firebasedatabase.app', + }, + }); + expect(instance).to.equal('https://foo.euw1.firebasedatabase.app'); + expect(path).to.equal('/bar'); + }); + it('should throw an error if the given instance name contains anything except alphanumerics and dashes', () => { expect(() => { - return database.resourceToInstanceAndPath( - 'projects/_/instances/a.bad.name/refs/bar' - ); + return database.extractInstanceAndPath({ + context: { + resource: { + name: 'projects/_/instances/a.bad.name/refs/bar', + }, + }, + }); }).to.throw(Error); expect(() => { - return database.resourceToInstanceAndPath( - 'projects/_/instances/a_different_bad_name/refs/bar' - ); + return database.extractInstanceAndPath({ + context: { + resource: { + name: 'projects/_/instances/a_different_bad_name/refs/bar', + }, + }, + }); }).to.throw(Error); expect(() => { - return database.resourceToInstanceAndPath( - 'projects/_/instances/BAD!!!!/refs/bar' - ); + return database.extractInstanceAndPath({ + context: { + resource: { + name: 'projects/_/instances/BAD!!!!/refs/bar', + }, + }, + }); }).to.throw(Error); }); }); @@ -457,9 +499,14 @@ describe('Database Functions', () => { const apps = new appsNamespace.Apps(); const populate = (data: any) => { - const [instance, path] = database.resourceToInstanceAndPath( - 'projects/_/instances/other-subdomain/refs/foo' - ); + const [instance, path] = database.extractInstanceAndPath({ + context: { + resource: { + name: 'projects/_/instances/other-subdomain/refs/foo', + }, + domain: 'firebaseio-staging.com', + }, + }); subject = new database.DataSnapshot(data, path, apps.admin, instance); }; @@ -467,7 +514,7 @@ describe('Database Functions', () => { it('should return a ref for correct instance, not the default instance', () => { populate({}); expect(subject.ref.toJSON()).to.equal( - 'https://other-subdomain.firebaseio.com/foo' + 'https://other-subdomain.firebaseio-staging.com/foo' ); }); }); @@ -648,9 +695,13 @@ describe('Database Functions', () => { }); it('should return null for the root', () => { - const [instance, path] = database.resourceToInstanceAndPath( - 'projects/_/instances/foo/refs/' - ); + const [instance, path] = database.extractInstanceAndPath({ + context: { + resource: { + name: 'projects/_/instances/foo/refs/', + }, + }, + }); const snapshot = new database.DataSnapshot( null, path, diff --git a/src/providers/database.ts b/src/providers/database.ts index 78614a152..c363981b6 100644 --- a/src/providers/database.ts +++ b/src/providers/database.ts @@ -215,9 +215,7 @@ export class RefBuilder { ) => PromiseLike | any ): CloudFunction { const dataConstructor = (raw: Event) => { - const [dbInstance, path] = resourceToInstanceAndPath( - raw.context.resource.name - ); + const [dbInstance, path] = extractInstanceAndPath(raw); return new DataSnapshot( raw.data.delta, path, @@ -243,9 +241,7 @@ export class RefBuilder { ) => PromiseLike | any ): CloudFunction { const dataConstructor = (raw: Event) => { - const [dbInstance, path] = resourceToInstanceAndPath( - raw.context.resource.name - ); + const [dbInstance, path] = extractInstanceAndPath(raw); return new DataSnapshot(raw.data.data, path, this.apps.admin, dbInstance); }; return this.onOperation(handler, 'ref.delete', dataConstructor); @@ -271,9 +267,7 @@ export class RefBuilder { } private changeConstructor = (raw: Event): Change => { - const [dbInstance, path] = resourceToInstanceAndPath( - raw.context.resource.name - ); + const [dbInstance, path] = extractInstanceAndPath(raw); const before = new DataSnapshot( raw.data.data, path, @@ -293,9 +287,13 @@ export class RefBuilder { }; } -/* Utility function to extract database reference from resource string */ +/** + * Utility function to extract database reference from resource string. + * The raw event may contain fields not part of the public API. + */ /** @hidden */ -export function resourceToInstanceAndPath(resource: string) { +export function extractInstanceAndPath(rawEvent: any) { + const resource = rawEvent.context.resource.name; const resourceRegex = `projects/([^/]+)/instances/([a-zA-Z0-9\-^/]+)/refs(/.+)?`; const match = resource.match(new RegExp(resourceRegex)); if (!match) { @@ -310,7 +308,14 @@ export function resourceToInstanceAndPath(resource: string) { `Expect project to be '_' in a Firebase Realtime Database event` ); } - const dbInstance = 'https://' + dbInstanceName + '.firebaseio.com'; + let domain = 'firebaseio.com'; + if (_.has(rawEvent, 'context.domain')) { + // See go/rtdb-multi-region-function-sdk. + // Multi-region RTDB are served from different domains. + // Since region is not part of the resource name, it is provided through context. + domain = _.get(rawEvent, 'context.domain'); + } + const dbInstance = 'https://' + dbInstanceName + '.' + domain; return [dbInstance, path]; }