Skip to content

Add prototype for Firestore module #948

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

Closed
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
4 changes: 2 additions & 2 deletions src/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import {URL} from 'url';
import * as path from 'path';

import {FirebaseApp, app as defaultApp} from '../index';
import {FirebaseApp, app as defaultApp, SDK_VERSION} from '../index';
import {FirebaseDatabaseError, AppErrorCodes, FirebaseAppError} from '../utils/error';
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
import {Database} from '@firebase/database';
Expand Down Expand Up @@ -98,7 +98,7 @@ export class DatabaseService implements FirebaseServiceInterface {
if (typeof db === 'undefined') {
const rtdb = require('@firebase/database'); // eslint-disable-line @typescript-eslint/no-var-requires
// const { version } = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires
const version = 'XXX_SDK_VERSION_XXX';
const version = SDK_VERSION;
db = rtdb.initStandalone(this.appInternal, dbUrl, version).instance;

const rulesClient = new DatabaseRulesClient(this.app, dbUrl);
Expand Down
134 changes: 134 additions & 0 deletions src/firestore/firestore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*!
* Copyright 2020 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { FirebaseApp, app as defaultApp } from '../index';
import { FirebaseFirestoreError } from '../utils/error';
import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service';
import { isApplicationDefault } from '../auth/credential';
import { ServiceAccountCredential } from '../auth/credential-internal';
import * as firestoreCloud from '@google-cloud/firestore';

import * as validator from '../utils/validator';
import * as utils from '../utils/index';

const Firestore_: { [name: string]: FirestoreService } = {};

/**
* Internals of a Firestore instance.
*/
class FirestoreInternals implements FirebaseServiceInternalsInterface {
/**
* Deletes the service and its associated resources.
*
* @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted.
*/
public delete(): Promise<void> {
// There are no resources to clean up.
return Promise.resolve();
}
}

export class FirestoreService implements FirebaseServiceInterface {
public INTERNAL: FirestoreInternals = new FirestoreInternals();

private appInternal: FirebaseApp;
private firestoreClient: firestoreCloud.Firestore;

constructor(app: FirebaseApp) {
this.firestoreClient = initFirestore(app);
this.appInternal = app;
}

/**
* Returns the app associated with this Storage instance.
*
* @return {FirebaseApp} The app associated with this Storage instance.
*/
get app(): FirebaseApp {
return this.appInternal;
}

get client(): firestoreCloud.Firestore {
return this.firestoreClient;
}
}

export function getFirestoreOptions(app: FirebaseApp): firestoreCloud.Settings {
if (!validator.isNonNullObject(app) || !('options' in app)) {
throw new FirebaseFirestoreError({
code: 'invalid-argument',
message: 'First argument passed to admin.firestore() must be a valid Firebase app instance.',
});
}

const projectId: string | null = utils.getExplicitProjectId(app);
const credential = app.options.credential;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version: firebaseVersion } = require('../../package.json');
if (credential instanceof ServiceAccountCredential) {
return {
credentials: {
private_key: credential.privateKey, // eslint-disable-line @typescript-eslint/camelcase
client_email: credential.clientEmail, // eslint-disable-line @typescript-eslint/camelcase
},
// When the SDK is initialized with ServiceAccountCredentials an explicit projectId is
// guaranteed to be available.
projectId: projectId!,
firebaseVersion,
};
} else if (isApplicationDefault(app.options.credential)) {
// Try to use the Google application default credentials.
// If an explicit project ID is not available, let Firestore client discover one from the
// environment. This prevents the users from having to set GOOGLE_CLOUD_PROJECT in GCP runtimes.
return validator.isNonEmptyString(projectId) ? { projectId, firebaseVersion } : { firebaseVersion };
}

throw new FirebaseFirestoreError({
code: 'invalid-credential',
message: 'Failed to initialize Google Cloud Firestore client with the available credentials. ' +
'Must initialize the SDK with a certificate credential or application default credentials ' +
'to use Cloud Firestore API.',
});
}

function initFirestore(app: FirebaseApp): firestoreCloud.Firestore {
const options = getFirestoreOptions(app);
let firestoreDatabase: typeof firestoreCloud.Firestore;
try {
// Lazy-load the Firestore implementation here, which in turns loads gRPC.
firestoreDatabase = require('@google-cloud/firestore').Firestore;
} catch (err) {
throw new FirebaseFirestoreError({
code: 'missing-dependencies',
message: 'Failed to import the Cloud Firestore client library for Node.js. '
+ 'Make sure to install the "@google-cloud/firestore" npm package. '
+ `Original error: ${err}`,
});
}

return new firestoreDatabase(options);
}

export function firestore(app?: FirebaseApp): firestoreCloud.Firestore {
if (typeof (app) === 'undefined') {
app = defaultApp();
}
if (!(app.name in Firestore_)) {
Firestore_[app.name] = new FirestoreService(app);
}
app.registerService('firestore', Firestore_[app.name]);
return Firestore_[app.name].client;
}
36 changes: 36 additions & 0 deletions src/firestore/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*!
* Copyright 2020 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './firestore';

import * as cloudFirestore from '@google-cloud/firestore';
export import v1beta1 = cloudFirestore.v1beta1;
export import v1 = cloudFirestore.v1;
export import CollectionReference = cloudFirestore.CollectionReference;
export import DocumentData = cloudFirestore.DocumentData;
export import DocumentReference = cloudFirestore.DocumentReference;
export import DocumentSnapshot = cloudFirestore.DocumentSnapshot;
export import FieldPath = cloudFirestore.FieldPath;
export import FieldValue = cloudFirestore.FieldValue;
export import Firestore = cloudFirestore.Firestore;
export import GeoPoint = cloudFirestore.GeoPoint;
export import Query = cloudFirestore.Query;
export import QueryDocumentSnapshot = cloudFirestore.QueryDocumentSnapshot;
export import QuerySnapshot = cloudFirestore.QuerySnapshot;
export import Timestamp = cloudFirestore.Timestamp;
export import Transaction = cloudFirestore.Transaction;
export import WriteBatch = cloudFirestore.WriteBatch;
export import WriteResult = cloudFirestore.WriteResult;
export import setLogFunction = cloudFirestore.setLogFunction;
3 changes: 2 additions & 1 deletion test/integration/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ describe('admin', () => {
expect(storageBucket).to.be.not.empty;
});

/*
it('does not load Firestore by default', () => {
const gcloud = require.cache[require.resolve('@google-cloud/firestore')];
expect(gcloud).to.be.undefined;
});
}); */
});

describe('admin.app', () => {
Expand Down
157 changes: 157 additions & 0 deletions test/integration/firestore.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*!
* Copyright 2018 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// import * as admin from '../../lib/index';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { defaultApp } from './setup';
import { DocumentReference, CollectionReference, Firestore, firestore, FieldPath, FieldValue, Timestamp, WriteBatch, GeoPoint, WriteResult, setLogFunction} from '../../lib/firestore/';

import { clone } from 'lodash';

chai.should();
chai.use(chaiAsPromised);

const expect = chai.expect;

const mountainView = {
name: 'Mountain View',
population: 77846,
};

describe('admin.firestore', () => {

let reference: DocumentReference;

before(() => {
const db = firestore(defaultApp);
reference = db.collection('cities').doc();
});

it('admin.firestore() returns a Firestore client', () => {
const fs = firestore(defaultApp);
expect(fs).to.be.instanceOf(Firestore);
});

it('app.firestore() returns a Firestore client', () => {
const fs = firestore(defaultApp);
expect(fs).to.be.instanceOf(Firestore);
// TEMPORARY
expect(reference).to.not.be.null;
});

it('supports basic data access', () => {
return reference.set(mountainView)
.then(() => {
return reference.get();
})
.then((snapshot) => {
const data = snapshot.data();
expect(data).to.deep.equal(mountainView);
return reference.delete();
})
.then(() => {
return reference.get();
})
.then((snapshot) => {
expect(snapshot.exists).to.be.false;
});
});

it('admin.firestore.FieldValue.serverTimestamp() provides a server-side timestamp', () => {
const expected: any = clone(mountainView);
expected.timestamp = FieldValue.serverTimestamp();
return reference.set(expected)
.then(() => {
return reference.get();
})
.then((snapshot) => {
const data = snapshot.data();
expect(data).to.exist;
expect(data!.timestamp).is.not.null;
expect(data!.timestamp).to.be.instanceOf(Timestamp);
return reference.delete();
})
.should.eventually.be.fulfilled;
});

it('admin.firestore.CollectionReference type is defined', () => {
expect(typeof CollectionReference).to.be.not.undefined;
});

it('admin.firestore.FieldPath type is defined', () => {
expect(typeof FieldPath).to.be.not.undefined;
});

it('admin.firestore.FieldValue type is defined', () => {
expect(typeof FieldValue).to.be.not.undefined;
});

it('admin.firestore.GeoPoint type is defined', () => {
expect(typeof GeoPoint).to.be.not.undefined;
});

it('admin.firestore.Timestamp type is defined', () => {
const now = Timestamp.now();
expect(typeof now.seconds).to.equal('number');
expect(typeof now.nanoseconds).to.equal('number');
});

it('admin.firestore.WriteBatch type is defined', () => {
expect(typeof WriteBatch).to.be.not.undefined;
});

it('admin.firestore.WriteResult type is defined', () => {
expect(typeof WriteResult).to.be.not.undefined;
});

it('supports saving references in documents', () => {
const source = firestore(defaultApp).collection('cities').doc();
const target = firestore(defaultApp).collection('cities').doc();
return source.set(mountainView)
.then(() => {
return target.set({ name: 'Palo Alto', sisterCity: source });
})
.then(() => {
return target.get();
})
.then((snapshot) => {
const data = snapshot.data();
expect(data).to.exist;
expect(data!.sisterCity.path).to.deep.equal(source.path);
const promises = [];
promises.push(source.delete());
promises.push(target.delete());
return Promise.all(promises);
})
.should.eventually.be.fulfilled;
});

it('admin.firestore.setLogFunction() enables logging for the Firestore module', () => {
const logs: string[] = [];
const source = firestore(defaultApp).collection('cities').doc();
setLogFunction((log) => {
logs.push(log);
});
return source.set({ name: 'San Francisco' })
.then(() => {
return source.delete();
})
.then(() => {
expect(logs.length).greaterThan(0);
});
});
});
Loading