Skip to content

Commit 72ab108

Browse files
authored
Firestore API (#94)
* Prototyped Firestore integration * Added integration tests * Fixing a test case * Exposing Firestore client directly from admin.firestore() * Merged with master; Updated tests * Updated documentation and fixed some typos; Introduced FirebaseFirestoreError type; Fixed index.d.ts by exporting Firestore as any; Added unit tests for utils.getProjectId() * Fixing an incorrect test description * Using Firestore type definitions * Using auto IDs in integration test * Some minor changes that enables building RCs from this branch. Some of these changes can be removed once the Firestore is officially released. * Supporting Firestore initialization with application default credentials, without a project ID. * Fixing up some nits * Exposing Firestore Types from admin.firestore Namespace (#5) * Upgraded to latest Firestore; Exposing FieldValue and few others via admin.firestore * Fixing Admin SDK type definitions for Firestore * Exporting all Firestore types via admin.firestore * Using lodash.assign() which only copies 'own' members and none of the inheritted members. * Declaring Firestore type in terms of admin.firestore.Firestore; More integration tests for admin.firestore namespace * Making GCS types a required dependency * Updated the Firestore dependency * Fixing a Node5 test failure * Updated shrinkwrap * Fixing NPM shrinkwrap/dependency issues
1 parent 05078a9 commit 72ab108

21 files changed

+5983
-49
lines changed

npm-shrinkwrap.json

Lines changed: 5420 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,15 @@
4848
],
4949
"types": "./lib/index.d.ts",
5050
"dependencies": {
51+
"google-auth-library": "^0.10.0",
5152
"@google-cloud/storage": "^1.2.1",
53+
"@google-cloud/firestore": "^0.8.0",
54+
"@types/google-cloud__storage": "^1.1.1",
5255
"@types/jsonwebtoken": "^7.1.33",
5356
"faye-websocket": "0.9.3",
5457
"jsonwebtoken": "7.1.9",
55-
"node-forge": "0.7.1"
58+
"node-forge": "0.7.1",
59+
"lodash": "^4.6.1"
5660
},
5761
"devDependencies": {
5862
"@types/chai": "^3.4.34",
@@ -61,6 +65,7 @@
6165
"@types/lodash": "^4.14.38",
6266
"@types/mocha": "^2.2.32",
6367
"@types/nock": "^8.0.33",
68+
"@types/node": "^8.0.32",
6469
"@types/request": "0.0.32",
6570
"@types/request-promise": "^4.1.33",
6671
"@types/sinon": "^1.16.31",
@@ -71,7 +76,6 @@
7176
"del": "^2.2.1",
7277
"firebase": "^3.6.9",
7378
"firebase-token-generator": "^2.0.0",
74-
"@types/google-cloud__storage": "^1.1.1",
7579
"gulp": "^3.9.1",
7680
"gulp-exit": "0.0.2",
7781
"gulp-header": "^1.8.8",
@@ -80,7 +84,6 @@
8084
"gulp-replace": "^0.5.4",
8185
"gulp-tslint": "^6.0.2",
8286
"gulp-typescript": "^3.1.2",
83-
"lodash": "^4.6.1",
8487
"merge2": "^1.0.2",
8588
"mocha": "^3.5.0",
8689
"nock": "^8.0.0",
@@ -91,8 +94,8 @@
9194
"run-sequence": "^1.1.5",
9295
"sinon": "^1.17.3",
9396
"sinon-chai": "^2.8.0",
94-
"tslint": "^3.5.0",
9597
"ts-node": "^3.3.0",
98+
"tslint": "^3.5.0",
9699
"typescript": "^2.0.3",
97100
"typings": "^1.0.4"
98101
}

src/firebase-app.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {GoogleOAuthAccessToken} from './auth/credential';
2121
import {FirebaseServiceInterface} from './firebase-service';
2222
import {FirebaseNamespaceInternals} from './firebase-namespace';
2323
import {AppErrorCodes, FirebaseAppError} from './utils/error';
24+
import {Firestore} from '@google-cloud/firestore';
2425

2526

2627
/**
@@ -37,6 +38,7 @@ export type FirebaseAppOptions = {
3738
databaseAuthVariableOverride?: Object
3839
databaseURL?: string,
3940
storageBucket?: string,
41+
projectId?: string,
4042
};
4143

4244
/**
@@ -310,6 +312,14 @@ export class FirebaseApp {
310312
);
311313
}
312314

315+
/* istanbul ignore next */
316+
public firestore(): Firestore {
317+
throw new FirebaseAppError(
318+
AppErrorCodes.INTERNAL_ERROR,
319+
'INTERNAL ASSERT FAILED: Firebase firestore() service has not been registered.',
320+
);
321+
}
322+
313323
/**
314324
* Returns the name of the FirebaseApp instance.
315325
*

src/firebase-namespace.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
RefreshTokenCredential,
2525
ApplicationDefaultCredential,
2626
} from './auth/credential';
27+
import {Firestore} from '@google-cloud/firestore';
2728

2829
const DEFAULT_APP_NAME = '[DEFAULT]';
2930

@@ -308,6 +309,14 @@ export class FirebaseNamespace {
308309
);
309310
}
310311

312+
/* istanbul ignore next */
313+
public firestore(): Firestore {
314+
throw new FirebaseAppError(
315+
AppErrorCodes.INTERNAL_ERROR,
316+
'INTERNAL ASSERT FAILED: Firebase firestore() service has not been registered.',
317+
);
318+
}
319+
311320
/**
312321
* Initializes the FirebaseApp instance.
313322
*

src/firestore/firestore.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*!
2+
* Copyright 2017 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {FirebaseApp} from '../firebase-app';
18+
import {FirebaseFirestoreError} from '../utils/error';
19+
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
20+
import {ApplicationDefaultCredential, Certificate} from '../auth/credential';
21+
import {Firestore} from '@google-cloud/firestore';
22+
23+
import * as validator from '../utils/validator';
24+
import * as utils from '../utils/index';
25+
26+
/**
27+
* Internals of a Firestore instance.
28+
*/
29+
class FirestoreInternals implements FirebaseServiceInternalsInterface {
30+
/**
31+
* Deletes the service and its associated resources.
32+
*
33+
* @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted.
34+
*/
35+
public delete(): Promise<void> {
36+
// There are no resources to clean up.
37+
return Promise.resolve();
38+
}
39+
}
40+
41+
function initFirestore(app: FirebaseApp): Firestore {
42+
if (!validator.isNonNullObject(app) || !('options' in app)) {
43+
throw new FirebaseFirestoreError({
44+
code: 'invalid-argument',
45+
message: 'First argument passed to admin.firestore() must be a valid Firebase app instance.',
46+
});
47+
}
48+
49+
const projectId: string = utils.getProjectId(app);
50+
const cert: Certificate = app.options.credential.getCertificate();
51+
let options: any;
52+
if (cert != null) {
53+
// cert is available when the SDK has been initialized with a service account JSON file,
54+
// or by setting the GOOGLE_APPLICATION_CREDENTIALS envrionment variable.
55+
56+
if (!validator.isNonEmptyString(projectId)) {
57+
// Assert for an explicit projct ID (either via AppOptions or the cert itself).
58+
throw new FirebaseFirestoreError({
59+
code: 'no-project-id',
60+
message: 'Failed to determine project ID for Firestore. Initialize the '
61+
+ 'SDK with service account credentials or set project ID as an app option. '
62+
+ 'Alternatively set the GCLOUD_PROJECT environment variable.',
63+
});
64+
}
65+
options = {
66+
credentials: {
67+
private_key: cert.privateKey,
68+
client_email: cert.clientEmail,
69+
},
70+
projectId,
71+
};
72+
} else if (app.options.credential instanceof ApplicationDefaultCredential) {
73+
// Try to use the Google application default credentials.
74+
if (validator.isNonEmptyString(projectId)) {
75+
options = {projectId};
76+
} else {
77+
// If an explicit project ID is not available, let Firestore client discover one from the
78+
// environment. This prevents the users from having to set GCLOUD_PROJECT in GCP runtimes.
79+
options = {};
80+
}
81+
} else {
82+
throw new FirebaseFirestoreError({
83+
code: 'invalid-credential',
84+
message: 'Failed to initialize Google Cloud Firestore client with the available credentials. ' +
85+
'Must initialize the SDK with a certificate credential or application default credentials ' +
86+
'to use Cloud Firestore API.',
87+
});
88+
}
89+
return new Firestore(options);
90+
}
91+
92+
/**
93+
* Creates a new Firestore service instance for the given FirebaseApp.
94+
*
95+
* @param {FirebaseApp} app The App for this Firestore service.
96+
* @return {FirebaseServiceInterface} A Firestore service instance.
97+
*/
98+
export function initFirestoreService(app: FirebaseApp): FirebaseServiceInterface {
99+
let firestore: any = initFirestore(app);
100+
101+
// Extend the Firestore client object so it implements FirebaseServiceInterface.
102+
utils.addReadonlyGetter(firestore, 'app', app);
103+
firestore.INTERNAL = new FirestoreInternals();
104+
return firestore;
105+
}

src/firestore/register-firestore.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {initFirestoreService} from './firestore';
2+
import {AppHook, FirebaseApp} from '../firebase-app';
3+
import {FirebaseServiceInterface} from '../firebase-service';
4+
import * as firebase from '../default-namespace';
5+
import {FirebaseServiceNamespace} from '../firebase-namespace';
6+
import * as firestoreNamespace from '@google-cloud/firestore';
7+
import * as _ from 'lodash/object';
8+
9+
/**
10+
* Factory function that creates a new Firestore service.
11+
*
12+
* @param {Object} app The app for this service.
13+
* @param {function(Object)} extendApp An extend function to extend the app namespace.
14+
*
15+
* @return {Firestore} The Firestore service for the specified app.
16+
*/
17+
function serviceFactory(app: FirebaseApp, extendApp: (props: Object) => void): FirebaseServiceInterface {
18+
return initFirestoreService(app);
19+
}
20+
21+
/**
22+
* Handles app life-cycle events.
23+
*
24+
* @param {string} event The app event that is occurring.
25+
* @param {FirebaseApp} app The app for which the app hook is firing.
26+
*/
27+
let appHook: AppHook = (event: string, app: FirebaseApp) => {
28+
return;
29+
};
30+
31+
export default function(): FirebaseServiceNamespace<FirebaseServiceInterface> {
32+
return firebase.INTERNAL.registerService(
33+
'firestore',
34+
serviceFactory,
35+
_.assign({}, firestoreNamespace),
36+
appHook
37+
);
38+
}

src/index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import {Bucket} from '@google-cloud/storage';
18+
import * as _firestore from '@google-cloud/firestore';
1819

1920
declare namespace admin {
2021
interface FirebaseError {
@@ -56,6 +57,7 @@ declare namespace admin {
5657
function database(app?: admin.app.App): admin.database.Database;
5758
function messaging(app?: admin.app.App): admin.messaging.Messaging;
5859
function storage(app?: admin.app.App): admin.storage.Storage;
60+
function firestore(app?: admin.app.App): admin.firestore.Firestore;
5961
function initializeApp(options: admin.AppOptions, name?: string): admin.app.App;
6062
}
6163

@@ -66,6 +68,7 @@ declare namespace admin.app {
6668

6769
auth(): admin.auth.Auth;
6870
database(): admin.database.Database;
71+
firestore(): admin.firestore.Firestore;
6972
messaging(): admin.messaging.Messaging;
7073
storage(): admin.storage.Storage;
7174
delete(): Promise<void>;
@@ -402,6 +405,13 @@ declare namespace admin.storage {
402405
}
403406
}
404407

408+
declare namespace admin.firestore {
409+
export import FieldPath = _firestore.FieldPath;
410+
export import FieldValue = _firestore.FieldValue;
411+
export import Firestore = _firestore.Firestore;
412+
export import GeoPoint = _firestore.GeoPoint;
413+
}
414+
405415
declare module 'firebase-admin' {
406416
}
407417

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import * as firebase from './default-namespace';
1818
import registerAuth from './auth/register-auth';
1919
import registerMessaging from './messaging/register-messaging';
2020
import registerStorage from './storage/register-storage';
21+
import registerFirestore from './firestore/register-firestore';
2122

2223
// Register the Database service
2324
// For historical reasons, the database code is included as minified code and registers itself
@@ -35,4 +36,7 @@ registerMessaging();
3536
// Register the Storage service
3637
registerStorage();
3738

39+
// Register the Firestore service
40+
registerFirestore();
41+
3842
export = firebase;

src/storage/storage.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import {FirebaseApp} from '../firebase-app';
1818
import {FirebaseError} from '../utils/error';
1919
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
20-
import {ApplicationDefaultCredential} from '../auth/credential';
20+
import {ApplicationDefaultCredential, Certificate} from '../auth/credential';
2121
import {Bucket} from '@google-cloud/storage';
2222

2323
import * as validator from '../utils/validator';
@@ -47,7 +47,7 @@ export class Storage implements FirebaseServiceInterface {
4747
private storageClient: any;
4848

4949
/**
50-
* @param {Object} app The app for this Storage service.
50+
* @param {FirebaseApp} app The app for this Storage service.
5151
* @constructor
5252
*/
5353
constructor(app: FirebaseApp) {
@@ -70,7 +70,7 @@ export class Storage implements FirebaseServiceInterface {
7070
});
7171
}
7272

73-
const cert = app.options.credential.getCertificate();
73+
const cert: Certificate = app.options.credential.getCertificate();
7474
if (cert != null) {
7575
// cert is available when the SDK has been initialized with a service account JSON file,
7676
// or by setting the GOOGLE_APPLICATION_CREDENTIALS envrionment variable.
@@ -94,6 +94,14 @@ export class Storage implements FirebaseServiceInterface {
9494
this.appInternal = app;
9595
}
9696

97+
/**
98+
* Returns a reference to a Google Cloud Storage bucket. Returned reference can be used to upload
99+
* and download content from Google Cloud Storage.
100+
*
101+
* @param {string=} name Optional name of the bucket to be retrieved. If name is not specified,
102+
* retrieves a reference to the default bucket.
103+
* @return {Bucket} A Bucket object from the @google-cloud/storage library.
104+
*/
97105
public bucket(name?: string): Bucket {
98106
let bucketName;
99107
if (typeof name !== 'undefined') {

src/utils/error.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,21 @@ export class FirebaseAuthError extends PrefixedFirebaseError {
176176
}
177177
}
178178

179+
/**
180+
* Firebase Firestore error code structure. This extends FirebaseError.
181+
*
182+
* @param {ErrorInfo} info The error code info.
183+
* @param {string} [message] The error message. This will override the default
184+
* message if provided.
185+
* @constructor
186+
*/
187+
export class FirebaseFirestoreError extends FirebaseError {
188+
constructor(info: ErrorInfo, message?: string) {
189+
// Override default message if custom message provided.
190+
super({code: 'firestore/' + info.code, message: message || info.message});
191+
}
192+
}
193+
179194

180195
/**
181196
* Firebase Messaging error code structure. This extends PrefixedFirebaseError.

0 commit comments

Comments
 (0)