Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.

Commit e55174c

Browse files
Remove project ID replacement
1 parent 0b04319 commit e55174c

13 files changed

Lines changed: 517 additions & 473 deletions

File tree

dev/src/index.ts

Lines changed: 88 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {replaceProjectIdToken} from '@google-cloud/projectify';
18-
import * as assert from 'assert';
1917
import * as bun from 'bun';
20-
import * as extend from 'extend';
18+
import {CallOptions} from 'google-gax';
2119
import * as through2 from 'through2';
2220

2321
import {google} from '../protos/firestore_proto_api';
2422
import {fieldsFromJson, timestampFromJson} from './convert';
2523
import {DocumentSnapshot, DocumentSnapshotBuilder, QueryDocumentSnapshot} from './document';
2624
import {logger, setLibVersion} from './logger';
27-
import {FieldPath, validateResourcePath} from './path';
25+
import {DATABASE_ID, FieldPath, RelativePath, validateResourcePath} from './path';
2826
import {ResourcePath} from './path';
2927
import {ClientPool} from './pool';
3028
import {CollectionReference} from './reference';
@@ -207,7 +205,7 @@ export class Firestore {
207205
* to work around a connection limit of 100 concurrent requests per client.
208206
* @private
209207
*/
210-
private _clientPool: ClientPool<GapicClient>|null = null;
208+
private _clientPool: ClientPool<GapicClient>;
211209

212210
/**
213211
* The configuration options for the GAPIC client.
@@ -223,19 +221,19 @@ export class Firestore {
223221
private _settingsFrozen = false;
224222

225223
/**
226-
* A Promise that resolves when client initialization completes. Can be
227-
* 'null' if initialization hasn't started yet.
224+
* The serializer to use for the Protobuf transformation.
228225
* @private
229226
*/
230-
private _clientInitialized: Promise<void>|null = null;
227+
_serializer: Serializer|null = null;
231228

232229
/**
233-
* The serializer to use for the Protobuf transformation.
230+
* The project ID for this client.
231+
*
232+
* The project ID is auto-detected during the first request unless a project
233+
* ID is passed to the constructor (or provided via `.settings()`).
234234
* @private
235235
*/
236-
_serializer: Serializer|null = null;
237-
238-
private _referencePath: ResourcePath|null = null;
236+
private _projectId: string|undefined = undefined;
239237

240238
// GCF currently tears down idle connections after two minutes. Requests
241239
// that are issued after this period may fail. On GCF, we therefore issue
@@ -311,6 +309,13 @@ export class Firestore {
311309
logger('Firestore', null, 'Detected GCF environment');
312310
}
313311

312+
this._clientPool =
313+
new ClientPool(MAX_CONCURRENT_REQUESTS_PER_CLIENT, () => {
314+
const client = new module.exports.v1(this._settings);
315+
logger('Firestore', null, 'Initialized Firestore GAPIC Client');
316+
return client;
317+
});
318+
314319
logger('Firestore', null, 'Initialized Firestore');
315320
}
316321

@@ -331,16 +336,9 @@ export class Firestore {
331336
'settings.timestampsInSnapshots', settings.timestampsInSnapshots,
332337
{optional: true});
333338

334-
if (this._clientInitialized) {
335-
throw new Error(
336-
'Firestore has already been started and its settings can no longer ' +
337-
'be changed. You can only call settings() before calling any other ' +
338-
'methods on a Firestore object.');
339-
}
340-
341339
if (this._settingsFrozen) {
342340
throw new Error(
343-
'Firestore.settings() has already be called. You can only call ' +
341+
'Firestore has already been initialized. You can only call ' +
344342
'settings() once, and only before calling any other methods on a ' +
345343
'Firestore object.');
346344
}
@@ -357,28 +355,33 @@ export class Firestore {
357355

358356
if (settings && settings.projectId) {
359357
validateString('settings.projectId', settings.projectId);
360-
this._referencePath = new ResourcePath(settings.projectId, '(default)');
361-
} else {
362-
// Initialize a temporary reference path that will be overwritten during
363-
// project ID detection.
364-
this._referencePath = new ResourcePath('{{projectId}}', '(default)');
358+
this._projectId = settings.projectId;
365359
}
366360

367361
this._settings = settings;
368362
this._serializer = new Serializer(this);
369363
}
370364

371365
/**
372-
* The root path to the database.
366+
* Returns the Project ID for this Firestore instance. Validates that
367+
* `initializeIfNeeded()` was called before.
368+
*
369+
* @private
370+
*/
371+
get projectId(): string {
372+
this.ensureClientInitialized();
373+
return this._projectId!;
374+
}
375+
376+
/**
377+
* Returns the root path of the database. Validates that
378+
* `initializeIfNeeded()` was called before.
373379
*
374380
* @private
375381
*/
376382
get formattedName(): string {
377-
const components = [
378-
'projects', this._referencePath!.projectId, 'databases',
379-
this._referencePath!.databaseId
380-
];
381-
return components.join('/');
383+
this.ensureClientInitialized();
384+
return `projects/${this.projectId}/databases/${DATABASE_ID}`;
382385
}
383386

384387
/**
@@ -396,7 +399,7 @@ export class Firestore {
396399
doc(documentPath: string): DocumentReference {
397400
validateResourcePath('documentPath', documentPath);
398401

399-
const path = this._referencePath!.append(documentPath);
402+
const path = RelativePath.EMPTY.append(documentPath);
400403
if (!path.isDocument) {
401404
throw new Error(`Argument "documentPath" must point to a document, but was "${
402405
documentPath}". Your path does not contain an even number of components.`);
@@ -424,7 +427,7 @@ export class Firestore {
424427
collection(collectionPath: string): CollectionReference {
425428
validateResourcePath('collectionPath', collectionPath);
426429

427-
const path = this._referencePath!.append(collectionPath);
430+
const path = RelativePath.EMPTY.append(collectionPath);
428431
if (!path.isCollection) {
429432
throw new Error(`Argument "collectionPath" must point to a collection, but was "${
430433
collectionPath}". Your path does not contain an odd number of components.`);
@@ -592,7 +595,8 @@ export class Firestore {
592595
{optional: true, minValue: 1});
593596
}
594597

595-
return this._runTransaction(updateFunction, transactionOptions);
598+
return this.initializeIfNeeded().then(
599+
() => this._runTransaction(updateFunction, transactionOptions));
596600
}
597601

598602
_runTransaction<T>(
@@ -667,7 +671,7 @@ export class Firestore {
667671
* });
668672
*/
669673
listCollections() {
670-
const rootDocument = new DocumentReference(this, this._referencePath!);
674+
const rootDocument = new DocumentReference(this, RelativePath.EMPTY);
671675
return rootDocument.listCollections();
672676
}
673677

@@ -712,7 +716,8 @@ export class Firestore {
712716

713717
const {documents, fieldMask} =
714718
parseGetAllArguments(documentRefsOrReadOptions);
715-
return this.getAll_(documents, fieldMask, requestTag());
719+
return this.initializeIfNeeded().then(
720+
() => this.getAll_(documents, fieldMask, requestTag()));
716721
}
717722

718723
/**
@@ -813,13 +818,14 @@ export class Firestore {
813818
}
814819

815820
/**
816-
* Executes a new request using the first available GAPIC client.
821+
* Initializes the client if it is not already initialized. All methods in the
822+
* SDK can be used after this method completes.
817823
*
818824
* @private
825+
* @return A Promise that resolves when the client is initialized.
819826
*/
820-
private _runRequest<T>(op: (client: GapicClient) => Promise<T>): Promise<T> {
821-
// Initialize the client pool if this is the first request.
822-
if (!this._clientInitialized) {
827+
async initializeIfNeeded(): Promise<void> {
828+
if (!this._settingsFrozen) {
823829
// Nobody should set timestampsInSnapshots anymore, but the error depends
824830
// on whether they set it to true or false...
825831
if (this._settings.timestampsInSnapshots === true) {
@@ -850,87 +856,51 @@ export class Firestore {
850856
Please audit all existing usages of Date when you enable the new
851857
behavior.`);
852858
}
853-
this._clientInitialized = this._initClientPool().then(clientPool => {
854-
this._clientPool = clientPool;
855-
});
856859
}
857860

858-
return this._clientInitialized!.then(() => this._clientPool!.run(op));
859-
}
860-
861-
/**
862-
* Initializes the client pool and invokes Project ID detection. Returns a
863-
* Promise on completion.
864-
*
865-
* @private
866-
*/
867-
private _initClientPool(): Promise<ClientPool<GapicClient>> {
868-
assert(!this._clientInitialized, 'Client pool already initialized');
869-
870-
const clientPool =
871-
new ClientPool(MAX_CONCURRENT_REQUESTS_PER_CLIENT, () => {
872-
const client = new module.exports.v1(this._settings);
873-
logger('Firestore', null, 'Initialized Firestore GAPIC Client');
874-
return client;
875-
});
876-
877-
const projectIdProvided =
878-
this._referencePath!.projectId !== '{{projectId}}';
861+
this._settingsFrozen = true;
879862

880-
if (projectIdProvided) {
881-
return Promise.resolve(clientPool);
882-
} else {
883-
return clientPool.run(client => this._detectProjectId(client))
884-
.then(projectId => {
885-
this._referencePath =
886-
new ResourcePath(projectId, this._referencePath!.databaseId);
887-
return clientPool;
863+
if (this._projectId === undefined) {
864+
this._projectId = await this._clientPool.run(gapicClient => {
865+
return new Promise((resolve, reject) => {
866+
gapicClient.getProjectId((err: Error, projectId: string) => {
867+
if (err) {
868+
logger(
869+
'Firestore._detectProjectId', null,
870+
'Failed to detect project ID: %s', err);
871+
reject(err);
872+
} else {
873+
logger(
874+
'Firestore._detectProjectId', null, 'Detected project ID: %s',
875+
projectId);
876+
resolve(projectId);
877+
}
888878
});
879+
});
880+
});
889881
}
890882
}
891883

892884
/**
893-
* Auto-detects the Firestore Project ID.
894-
*
885+
* Ensures that the client has been fully initialized;
895886
* @private
896-
* @param gapicClient The Firestore GAPIC client.
897-
* @return A Promise that resolves with the Project ID.
898887
*/
899-
private _detectProjectId(gapicClient: GapicClient): Promise<string> {
900-
return new Promise((resolve, reject) => {
901-
gapicClient.getProjectId((err: Error, projectId: string) => {
902-
if (err) {
903-
logger(
904-
'Firestore._detectProjectId', null,
905-
'Failed to detect project ID: %s', err);
906-
reject(err);
907-
} else {
908-
logger(
909-
'Firestore._detectProjectId', null, 'Detected project ID: %s',
910-
projectId);
911-
resolve(projectId);
912-
}
913-
});
914-
});
888+
private ensureClientInitialized(): void {
889+
if (this._projectId === undefined) {
890+
// Note that we do not mention `initializeIfNeeded()` here to not leak the
891+
// private API.
892+
throw new Error('INTERNAL ERROR: Client is yet ready to issue requests.');
893+
}
915894
}
916895

917896
/**
918-
* Decorate the request options of an API request. This is used to replace
919-
* any `{{projectId}}` placeholders with the value detected from the user's
920-
* environment, if one wasn't provided manually.
921-
*
897+
* Returns GAX call options that set the cloud resource header.
922898
* @private
923899
*/
924-
_decorateRequest<T>(request: T): {request: T, gax: {}} {
925-
let decoratedRequest = extend(true, {}, request);
926-
decoratedRequest =
927-
replaceProjectIdToken(decoratedRequest, this._referencePath!.projectId);
928-
const decoratedGax: {
929-
otherArgs: {headers: {[k: string]: string}}
930-
} = {otherArgs: {headers: {}}};
931-
decoratedGax.otherArgs.headers[CLOUD_RESOURCE_HEADER] = this.formattedName;
932-
933-
return {request: decoratedRequest, gax: decoratedGax};
900+
private createCallOptions(): CallOptions {
901+
const gaxHeaders: CallOptions = {otherArgs: {headers: {}}};
902+
gaxHeaders.otherArgs!.headers[CLOUD_RESOURCE_HEADER] = this.formattedName;
903+
return gaxHeaders;
934904
}
935905

936906
/**
@@ -1132,16 +1102,15 @@ export class Firestore {
11321102
methodName: string, request: {}, requestTag: string,
11331103
allowRetries: boolean): Promise<T> {
11341104
const attempts = allowRetries ? MAX_REQUEST_RETRIES : 1;
1105+
const callOptions = this.createCallOptions();
11351106

1136-
return this._runRequest(gapicClient => {
1137-
const decorated = this._decorateRequest(request);
1107+
return this._clientPool.run(gapicClient => {
11381108
return this._retry(attempts, requestTag, () => {
11391109
return new Promise((resolve, reject) => {
11401110
logger(
1141-
'Firestore.request', requestTag, 'Sending request: %j',
1142-
decorated.request);
1111+
'Firestore.request', requestTag, 'Sending request: %j', request);
11431112
gapicClient[methodName](
1144-
decorated.request, decorated.gax, (err: GrpcError, result: T) => {
1113+
request, callOptions, (err: GrpcError, result: T) => {
11451114
if (err) {
11461115
logger(
11471116
'Firestore.request', requestTag, 'Received error:', err);
@@ -1178,17 +1147,16 @@ export class Firestore {
11781147
methodName: string, request: {}, requestTag: string,
11791148
allowRetries: boolean): Promise<NodeJS.ReadableStream> {
11801149
const attempts = allowRetries ? MAX_REQUEST_RETRIES : 1;
1150+
const callOptions = this.createCallOptions();
11811151

1182-
return this._runRequest(gapicClient => {
1183-
const decorated = this._decorateRequest(request);
1152+
return this._clientPool.run(gapicClient => {
11841153
return this._retry(attempts, requestTag, () => {
11851154
return new Promise<NodeJS.ReadableStream>((resolve, reject) => {
11861155
try {
11871156
logger(
11881157
'Firestore.readStream', requestTag,
1189-
'Sending request: %j', decorated.request);
1190-
const stream = gapicClient[methodName](
1191-
decorated.request, decorated.gax);
1158+
'Sending request: %j', request);
1159+
const stream = gapicClient[methodName](request, callOptions);
11921160
const logStream =
11931161
through2.obj(function(this, chunk, enc, callback) {
11941162
logger(
@@ -1229,26 +1197,14 @@ export class Firestore {
12291197
readWriteStream(
12301198
methodName: string, request: {}, requestTag: string,
12311199
allowRetries: boolean): Promise<NodeJS.ReadWriteStream> {
1232-
const self = this;
12331200
const attempts = allowRetries ? MAX_REQUEST_RETRIES : 1;
1201+
const callOptions = this.createCallOptions();
12341202

1235-
return this._runRequest(gapicClient => {
1236-
const decorated = this._decorateRequest(request);
1203+
return this._clientPool.run(gapicClient => {
12371204
return this._retry(attempts, requestTag, () => {
12381205
return Promise.resolve().then(() => {
12391206
logger('Firestore.readWriteStream', requestTag, 'Opening stream');
1240-
const requestStream = gapicClient[methodName](decorated.gax);
1241-
1242-
// The transform stream to assign the project ID.
1243-
const transform = through2.obj((chunk, encoding, callback) => {
1244-
const decoratedChunk = extend(true, {}, chunk);
1245-
replaceProjectIdToken(
1246-
decoratedChunk, self._referencePath!.projectId);
1247-
logger(
1248-
'Firestore.readWriteStream', requestTag,
1249-
'Streaming request: %j', decoratedChunk);
1250-
requestStream.write(decoratedChunk, encoding, callback);
1251-
});
1207+
const requestStream = gapicClient[methodName](callOptions);
12521208

12531209
const logStream = through2.obj(function(this, chunk, enc, callback) {
12541210
logger(
@@ -1258,7 +1214,7 @@ export class Firestore {
12581214
callback();
12591215
});
12601216

1261-
const resultStream = bun([transform, requestStream, logStream]);
1217+
const resultStream = bun([requestStream, logStream]);
12621218
return this._initializeStream(resultStream, requestTag, request);
12631219
});
12641220
});

0 commit comments

Comments
 (0)