Skip to content

Commit c83e047

Browse files
authoredJul 13, 2018
Add settings() method (#257)
1 parent 4b853af commit c83e047

File tree

7 files changed

+205
-52
lines changed

7 files changed

+205
-52
lines changed
 

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"generate-scaffolding": "repo-tools generate all",
4242
"system-test": "mocha build/system-test --timeout 600000",
4343
"conformance": "mocha build/conformance",
44-
"test-only": "nyc mocha build/test",
44+
"test-only": "GOOGLE_APPLICATION_CREDENTIALS=build/test/fake-certificate.json nyc mocha build/test",
4545
"test": "npm run test-only",
4646
"check": "gts check",
4747
"clean": "gts clean",

‎src/index.js

+94-35
Original file line numberDiff line numberDiff line change
@@ -220,15 +220,15 @@ const GRPC_UNAVAILABLE = 14;
220220
*/
221221
class Firestore {
222222
/**
223-
* @param {Object=} options - [Configuration object](#/docs).
224-
* @param {string=} options.projectId The Firestore Project ID. Can be
223+
* @param {Object=} settings - [Configuration object](#/docs).
224+
* @param {string=} settings.projectId The Firestore Project ID. Can be
225225
* omitted in environments that support `Application Default Credentials`
226226
* {@see https://cloud.google.com/docs/authentication}
227-
* @param {string=} options.keyFilename Local file containing the Service
227+
* @param {string=} settings.keyFilename Local file containing the Service
228228
* Account credentials. Can be omitted in environments that support
229229
* `Application Default Credentials`
230230
* {@see https://cloud.google.com/docs/authentication}
231-
* @param {boolean=} options.timestampsInSnapshots Enables the use of
231+
* @param {boolean=} settings.timestampsInSnapshots Enables the use of
232232
* `Timestamp`s for timestamp fields in `DocumentSnapshots`.<br/>
233233
* Currently, Firestore returns timestamp fields as `Date` but `Date` only
234234
* supports millisecond precision, which leads to truncation and causes
@@ -242,8 +242,8 @@ class Firestore {
242242
* default and this option will be removed so you should change your code to
243243
* use `Timestamp` now and opt-in to this new behavior as soon as you can.
244244
*/
245-
constructor(options) {
246-
options = extend({}, options, {
245+
constructor(settings) {
246+
settings = extend({}, settings, {
247247
libName: 'gccl',
248248
libVersion: libVersion,
249249
});
@@ -257,11 +257,12 @@ class Firestore {
257257
this._clientPool = null;
258258

259259
/**
260-
* The configuration options for the GAPIC client.
260+
* Whether the initialization settings can still be changed by invoking
261+
* `settings()`.
261262
* @private
262-
* @type {Object}
263+
* @type {boolean}
263264
*/
264-
this._initalizationOptions = options;
265+
this._settingsFrozen = false;
265266

266267
/**
267268
* A Promise that resolves when client initialization completes. Can be
@@ -271,6 +272,15 @@ class Firestore {
271272
*/
272273
this._clientInitialized = null;
273274

275+
/**
276+
* The configuration options for the GAPIC client.
277+
* @private
278+
* @type {Object}
279+
*/
280+
this._initalizationSettings = null;
281+
282+
this.validateAndApplySettings(settings);
283+
274284
// GCF currently tears down idle connections after two minutes. Requests
275285
// that are issued after this period may fail. On GCF, we therefore issue
276286
// these requests as part of a transaction so that we can safely retry until
@@ -285,43 +295,59 @@ class Firestore {
285295
Firestore.log('Firestore', null, 'Detected GCF environment');
286296
}
287297

288-
this._timestampsInSnapshotsEnabled = !!options.timestampsInSnapshots;
298+
Firestore.log('Firestore', null, 'Initialized Firestore');
299+
}
289300

290-
if (!this._timestampsInSnapshotsEnabled) {
291-
console.error(`
292-
The behavior for Date objects stored in Firestore is going to change
293-
AND YOUR APP MAY BREAK.
294-
To hide this warning and ensure your app does not break, you need to add the
295-
following code to your app before calling any other Cloud Firestore methods:
301+
/**
302+
* Specifies custom settings to be used to configure the `Firestore`
303+
* instance. Can only be invoked once and before any other Firestore method.
304+
*
305+
* If settings are provided via both `settings()` and the `Firestore`
306+
* constructor, both settings objects are merged and any settings provided via
307+
* `settings()` take precedence.
308+
*
309+
* @param {object} settings The settings to use for all Firestore operations.
310+
*/
311+
settings(settings) {
312+
validate.isObject('settings', settings);
313+
validate.isOptionalString('settings.projectId', settings.projectId);
314+
validate.isOptionalBoolean(
315+
'settings.timestampsInSnapshots', settings.timestampsInSnapshots);
296316

297-
const settings = {/* your settings... */ timestampsInSnapshots: true};
298-
const firestore = new Firestore(settings);
317+
if (this._clientInitialized) {
318+
throw new Error(
319+
'Firestore has already been started and its settings can no longer ' +
320+
'be changed. You can only call settings() before calling any other ' +
321+
'methods on a Firestore object.');
322+
}
299323

300-
With this change, timestamps stored in Cloud Firestore will be read back as
301-
Firebase Timestamp objects instead of as system Date objects. So you will also
302-
need to update code expecting a Date to instead expect a Timestamp. For example:
324+
if (this._settingsFrozen) {
325+
throw new Error(
326+
'Firestore.settings() has already be called. You can only call ' +
327+
'settings() once, and only before calling any other methods on a ' +
328+
'Firestore object.');
329+
}
303330

304-
// Old:
305-
const date = snapshot.get('created_at');
306-
// New:
307-
const timestamp = snapshot.get('created_at');
308-
const date = timestamp.toDate();
331+
const mergedSettings = extend({}, this._initalizationSettings, settings);
332+
this.validateAndApplySettings(mergedSettings);
333+
this._settingsFrozen = true;
334+
}
309335

310-
Please audit all existing usages of Date when you enable the new behavior. In a
311-
future release, the behavior will change to the new behavior, so if you do not
312-
follow these steps, YOUR APP MAY BREAK.`);
313-
}
336+
validateAndApplySettings(settings) {
337+
validate.isOptionalBoolean(
338+
'settings.timestampsInSnapshots', settings.timestampsInSnapshots);
339+
this._timestampsInSnapshotsEnabled = !!settings.timestampsInSnapshots;
314340

315-
if (options && options.projectId) {
316-
validate.isString('options.projectId', options.projectId);
317-
this._referencePath = new ResourcePath(options.projectId, '(default)');
341+
if (settings && settings.projectId) {
342+
validate.isString('settings.projectId', settings.projectId);
343+
this._referencePath = new ResourcePath(settings.projectId, '(default)');
318344
} else {
319345
// Initialize a temporary reference path that will be overwritten during
320346
// project ID detection.
321347
this._referencePath = new ResourcePath('{{projectId}}', '(default)');
322348
}
323349

324-
Firestore.log('Firestore', null, 'Initialized Firestore');
350+
this._initalizationSettings = settings;
325351
}
326352

327353
/**
@@ -429,6 +455,12 @@ follow these steps, YOUR APP MAY BREAK.`);
429455
* for existing documents, otherwise a DocumentSnapshot.
430456
*/
431457
snapshot_(documentOrName, readTime, encoding) {
458+
if (!this._initalizationSettings.projectId) {
459+
throw new Error(
460+
'Cannot use `snapshot_()` without a Project ID. Please provide a ' +
461+
'Project ID via `Firestore.settings()`.');
462+
}
463+
432464
let convertTimestamp;
433465
let convertDocument;
434466

@@ -750,9 +782,36 @@ follow these steps, YOUR APP MAY BREAK.`);
750782
// Initialize the client pool if this is the first request.
751783
if (!this._clientInitialized) {
752784
common = require('@google-cloud/common');
785+
786+
if (!this._timestampsInSnapshotsEnabled) {
787+
console.error(`
788+
The behavior for Date objects stored in Firestore is going to change
789+
AND YOUR APP MAY BREAK.
790+
To hide this warning and ensure your app does not break, you need to add the
791+
following code to your app before calling any other Cloud Firestore methods:
792+
793+
const firestore = new Firestore();
794+
const settings = {/* your settings... */ timestampsInSnapshots: true};
795+
firestore.settings(settings);
796+
797+
With this change, timestamps stored in Cloud Firestore will be read back as
798+
Firebase Timestamp objects instead of as system Date objects. So you will also
799+
need to update code expecting a Date to instead expect a Timestamp. For example:
800+
801+
// Old:
802+
const date = snapshot.get('created_at');
803+
// New:
804+
const timestamp = snapshot.get('created_at');
805+
const date = timestamp.toDate();
806+
807+
Please audit all existing usages of Date when you enable the new behavior. In a
808+
future release, the behavior will change to the new behavior, so if you do not
809+
follow these steps, YOUR APP MAY BREAK.`);
810+
}
811+
753812
this._clientInitialized = this._initClientPool().then(clientPool => {
754813
this._clientPool = clientPool;
755-
})
814+
});
756815
}
757816

758817
return this._clientInitialized.then(() => this._clientPool.run(op));

‎src/validate.js

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ module.exports = validators => {
7575
},
7676
object: is.object,
7777
string: is.string,
78+
boolean: is.boolean
7879
},
7980
validators);
8081

‎test/index.js

+85-13
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ const createInstance = require('../test/util/helpers').createInstance;
3232

3333
const PROJECT_ID = 'test-project';
3434
const DATABASE_ROOT = `projects/${PROJECT_ID}/databases/(default)`;
35+
const DEFAULT_SETTINGS = {
36+
projectId: PROJECT_ID,
37+
sslCreds: grpc.credentials.createInsecure(),
38+
keyFilename: './test/fake-certificate.json',
39+
timestampsInSnapshots: true
40+
};
3541

3642
// Change the argument to 'console.log' to enable debug output.
3743
Firestore.setLogFunction(() => {});
@@ -305,22 +311,64 @@ function stream() {
305311

306312
describe('instantiation', function() {
307313
it('creates instance', function() {
308-
let firestore = new Firestore({
309-
projectId: PROJECT_ID,
310-
sslCreds: grpc.credentials.createInsecure(),
311-
timestampsInSnapshots: true,
312-
keyFilename: './test/fake-certificate.json',
313-
});
314+
let firestore = new Firestore(DEFAULT_SETTINGS);
314315
assert(firestore instanceof Firestore);
315316
});
316317

318+
it('merges settings', function() {
319+
let firestore = new Firestore(DEFAULT_SETTINGS);
320+
firestore.settings({foo: 'bar'});
321+
322+
assert.equal(firestore._initalizationSettings.projectId, PROJECT_ID);
323+
assert.equal(firestore._initalizationSettings.foo, 'bar');
324+
});
325+
326+
it('can only call settings() once', function() {
327+
let firestore = new Firestore(DEFAULT_SETTINGS);
328+
firestore.settings({timestampsInSnapshots: true});
329+
330+
assert.throws(
331+
() => firestore.settings({}),
332+
/Firestore.settings\(\) has already be called. You can only call settings\(\) once, and only before calling any other methods on a Firestore object./);
333+
});
334+
335+
it('cannot change settings after client initialized', function() {
336+
let firestore = new Firestore(DEFAULT_SETTINGS);
337+
firestore._runRequest(() => Promise.resolve());
338+
339+
assert.throws(
340+
() => firestore.settings({}),
341+
/Firestore has already been started and its settings can no longer be changed. You can only call settings\(\) before calling any other methods on a Firestore object./);
342+
});
343+
344+
it('validates project ID is string', function() {
345+
assert.throws(() => {
346+
const settings = Object.assign({}, DEFAULT_SETTINGS, {
347+
projectId: 1337,
348+
});
349+
new Firestore(settings);
350+
}, /Argument "settings.projectId" is not a valid string/);
351+
352+
assert.throws(() => {
353+
new Firestore(DEFAULT_SETTINGS).settings({projectId: 1337});
354+
}, /Argument "settings.projectId" is not a valid string/);
355+
});
356+
357+
it('validates timestampsInSnapshots is boolean', function() {
358+
assert.throws(() => {
359+
const settings = Object.assign({}, DEFAULT_SETTINGS, {
360+
timestampsInSnapshots: 1337,
361+
});
362+
new Firestore(settings);
363+
}, /Argument "settings.timestampsInSnapshots" is not a valid boolean/);
364+
365+
assert.throws(() => {
366+
new Firestore(DEFAULT_SETTINGS).settings({timestampsInSnapshots: 1337});
367+
}, /Argument "settings.timestampsInSnapshots" is not a valid boolean/);
368+
});
369+
317370
it('uses project id from constructor', () => {
318-
let firestore = new Firestore({
319-
projectId: PROJECT_ID,
320-
sslCreds: grpc.credentials.createInsecure(),
321-
timestampsInSnapshots: true,
322-
keyFilename: './test/fake-certificate.json',
323-
});
371+
let firestore = new Firestore(DEFAULT_SETTINGS);
324372

325373
return firestore._runRequest(() => {
326374
assert.equal(
@@ -367,7 +415,20 @@ describe('instantiation', function() {
367415
});
368416
});
369417

370-
it('handles error from project ID detection', () => {
418+
it('uses project ID from settings()', function() {
419+
let firestore = new Firestore({
420+
sslCreds: grpc.credentials.createInsecure(),
421+
timestampsInSnapshots: true,
422+
keyFilename: './test/fake-certificate.json',
423+
});
424+
425+
firestore.settings({projectId: PROJECT_ID});
426+
427+
assert.equal(
428+
firestore.formattedName, `projects/${PROJECT_ID}/databases/(default)`);
429+
});
430+
431+
it('handles error from project ID detection', function() {
371432
let firestore = new Firestore({
372433
sslCreds: grpc.credentials.createInsecure(),
373434
timestampsInSnapshots: true,
@@ -479,6 +540,17 @@ describe('snapshot_() method', function() {
479540
});
480541
});
481542

543+
it('validates Project ID provided', function() {
544+
firestore = new Firestore({
545+
sslCreds: grpc.credentials.createInsecure(),
546+
keyFilename: './test/fake-certificate.json'
547+
});
548+
549+
assert.throws(
550+
() => firestore.snapshot_(),
551+
/Cannot use `snapshot_\(\)` without a Project ID. Please provide a Project ID via `Firestore.settings\(\)`./);
552+
});
553+
482554
it('handles ProtobufJS', function() {
483555
let doc = firestore.snapshot_(
484556
document('doc', {

‎test/typescript.ts

+6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ xdescribe('firestore.d.ts', function() {
5353
FirebaseFirestore.setLogFunction(console.log);
5454

5555
it('has typings for Firestore', () => {
56+
firestore.settings({
57+
keyFilename: 'foo',
58+
projectId: 'foo',
59+
timestampsInSnapshots: true,
60+
otherOption: 'foo'
61+
});
5662
const collRef: CollectionReference = firestore.collection('coll');
5763
const docRef1: DocumentReference = firestore.doc('coll/doc');
5864
const docRef2: DocumentReference = firestore.doc('coll/doc');

‎test/util/helpers.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ export function createInstance(
8080
},
8181
firestoreSettings);
8282

83-
const firestore = new Firestore(initializationOptions);
83+
const firestore = new Firestore();
84+
firestore.settings(initializationOptions);
8485

8586
const clientPool = new ClientPool(/* concurrentRequestLimit= */ 1, () => {
8687
const gapicClient: GapicClient = v1beta1(initializationOptions);

‎types/firestore.d.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,24 @@ declare namespace FirebaseFirestore {
8585
*/
8686
export class Firestore {
8787
/**
88-
* @param options - Configuration object. See [Firestore Documentation]
88+
* @param settings - Configuration object. See [Firestore Documentation]
8989
* {@link https://firebase.google.com/docs/firestore/}
9090
*/
91-
public constructor(options?: Settings);
91+
public constructor(settings?: Settings);
92+
93+
/**
94+
* Specifies custom settings to be used to configure the `Firestore`
95+
* instance. Can only be invoked once and before any other Firestore
96+
* method.
97+
*
98+
* If settings are provided via both `settings()` and the `Firestore`
99+
* constructor, both settings objects are merged and any settings provided
100+
* via `settings()` take precedence.
101+
*
102+
* @param {object} settings The settings to use for all Firestore
103+
* operations.
104+
*/
105+
settings(settings: Settings);
92106

93107
/**
94108
* Gets a `CollectionReference` instance that refers to the collection at

0 commit comments

Comments
 (0)
Please sign in to comment.