Skip to content

Commit 087375c

Browse files
authored
Revive Node.JS Support for Cloud Firestore (fixes #221). (#319)
* Fix miscellaneous node / GRPC bit rot. * Re-add grpc dependency (upgraded). * Avoid protobufjs dependency by loading protos via grpc. * Remove crazy grpc stream error handling nonsense that is no longer necessary after grpc/grpc#9101 * Clean up grpc_connection logging (and consistently use util.inspect()). * Fix WebChannel / GRPC Connection compatibility issues. * Add an explicit mapping from "RPC name" (e.g. "BatchGetDocuments") to the REST url path (e.g. "batchGet") for WebChannel, and for GRPC just assume the first letter should be lowercased (e.g. "batchGetDocuments"). * Split Connection.invoke() into invokeRPC() and invokeStreamingRPC(), with the latter accepting a stream of results and aggregating them into an array (needed to support BatchGetDocuments RPC). * Fix serializer issues * Query limits are an 'Int32Value' but we were serializing them as a normal int which GRPC / protobufjs didn't like. * Several proto "oneof tags" were outdated. * Add build steps to copy protos into npm package. * Run integration tests for Node.JS * Added to 'test:node' script in package.json and in .vscode/launch.json * Include index.ts for browser and index.node.ts for node so the appropriate PlatformSupport gets registered. * Misc cleanup * Remove unused MockPlatform since we use the normal NodePlatform now. * Remove 'google-auth-library' CredentialsProvider that we used to use for node.js (before we were integrated with FirebaseAuth). * Fixed several tests that were hitting node.js warnings about unhandled promise failures. * mocha commmand-line args: * "--compilers ts:ts-node/register" was deprecated in favor of "--require ts-node/register" * Remove "--retries 5" when running mocha tests from VS Code. * Consistently use "--require" instead of "-r" * Add "--exit" when running from VS Code.
1 parent 2e1b376 commit 087375c

28 files changed

+300
-377
lines changed

.vscode/launch.json

+22-6
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,27 @@
1111
"program": "${workspaceRoot}/packages/firestore/node_modules/.bin/_mocha",
1212
"cwd": "${workspaceRoot}/packages/firestore",
1313
"args": [
14-
"--compilers", "ts:ts-node/register",
15-
"-r", "src/platform_node/node_init.ts",
16-
"--retries", "5",
14+
"--require", "ts-node/register",
15+
"--require", "index.node.ts",
1716
"--timeout", "5000",
18-
"test/{,!(integration|browser)/**/}*.test.ts"
17+
"test/{,!(browser|integration)/**/}*.test.ts",
18+
"--exit"
19+
],
20+
"sourceMaps": true,
21+
"protocol": "inspector"
22+
},
23+
{
24+
"type": "node",
25+
"request": "launch",
26+
"name": "Firestore Integration Tests (Node)",
27+
"program": "${workspaceRoot}/packages/firestore/node_modules/.bin/_mocha",
28+
"cwd": "${workspaceRoot}/packages/firestore",
29+
"args": [
30+
"--require", "ts-node/register",
31+
"--require", "index.node.ts",
32+
"--timeout", "5000",
33+
"test/{,!(browser|unit)/**/}*.test.ts",
34+
"--exit"
1935
],
2036
"sourceMaps": true,
2137
"protocol": "inspector"
@@ -25,7 +41,7 @@
2541
"request": "launch",
2642
"name": "Firestore Unit Tests (Browser)",
2743
"program": "${workspaceRoot}/packages/firestore/node_modules/.bin/karma",
28-
"cwd": "${workspaceRoot}/packages/firestore",
44+
"cwd": "${workspaceRoot}/packages/firestore",
2945
"args": [
3046
"start",
3147
"--auto-watch",
@@ -38,7 +54,7 @@
3854
"request": "launch",
3955
"name": "Firestore Integration Tests (Browser)",
4056
"program": "${workspaceRoot}/packages/firestore/node_modules/.bin/karma",
41-
"cwd": "${workspaceRoot}/packages/firestore",
57+
"cwd": "${workspaceRoot}/packages/firestore",
4258
"args": [
4359
"start",
4460
"--auto-watch",

packages/firestore/gulpfile.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,19 @@
1717
const gulp = require('gulp');
1818
const tools = require('../../tools/build');
1919

20+
function copyProtos(dest) {
21+
return function copyProtos() {
22+
return gulp
23+
.src([__dirname + '/src/protos/**/*.proto'])
24+
.pipe(gulp.dest(dest));
25+
};
26+
}
27+
2028
const buildModule = gulp.parallel([
2129
tools.buildCjs(__dirname),
22-
tools.buildEsm(__dirname)
30+
copyProtos('dist/cjs/src/protos'),
31+
tools.buildEsm(__dirname),
32+
copyProtos('dist/esm/src/protos')
2333
]);
2434

2535
const setupWatcher = () => {

packages/firestore/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"test": "run-p test:browser test:node",
88
"test:browser": "karma start --single-run",
99
"test:browser:debug": "karma start --browsers=Chrome --auto-watch",
10-
"test:node": "nyc --reporter lcovonly -- mocha 'test/{,!(integration|browser)/**/}*.test.ts' --compilers ts:ts-node/register -r src/platform_node/node_init.ts --retries 5 --timeout 5000 --exit",
10+
"test:node": "nyc --reporter lcovonly -- mocha 'test/{,!(browser)/**/}*.test.ts' --require ts-node/register --require index.node.ts --retries 5 --timeout 5000 --exit",
1111
"prepare": "gulp build"
1212
},
1313
"main": "dist/cjs/index.node.js",
@@ -18,7 +18,8 @@
1818
"module": "dist/esm/index.js",
1919
"license": "Apache-2.0",
2020
"dependencies": {
21-
"@firebase/webchannel-wrapper": "^0.2.4"
21+
"@firebase/webchannel-wrapper": "^0.2.4",
22+
"grpc": "^1.7.1"
2223
},
2324
"peerDependencies": {
2425
"@firebase/app": "^0.1.0"
@@ -28,7 +29,6 @@
2829
"@types/mocha": "^2.2.44",
2930
"@types/sinon": "^2.3.7",
3031
"chai": "^4.1.1",
31-
"grpc": "^1.6.6",
3232
"gulp": "gulpjs/gulp#4.0",
3333
"karma": "^1.7.0",
3434
"karma-chrome-launcher": "^2.2.0",

packages/firestore/src/api/credentials.ts

-48
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,6 @@ export interface FirstPartyCredentialsSettings {
2929
sessionIndex: string;
3030
}
3131

32-
export interface GoogleAuthCredentialsSettings {
33-
type: 'google-auth';
34-
client: GoogleAuthClient;
35-
}
36-
3732
export interface ProviderCredentialsSettings {
3833
type: 'provider';
3934
client: CredentialsProvider;
@@ -42,7 +37,6 @@ export interface ProviderCredentialsSettings {
4237
/** Settings for private credentials */
4338
export type CredentialsSettings =
4439
| FirstPartyCredentialsSettings
45-
| GoogleAuthCredentialsSettings
4640
| ProviderCredentialsSettings;
4741

4842
export type TokenType = 'OAuth' | 'FirstParty';
@@ -238,45 +232,6 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
238232
}
239233
}
240234

241-
// Wrap a google-auth-library client as a CredentialsProvider.
242-
// NOTE: grpc-connection can natively accept a google-auth-library
243-
// client via createFromGoogleCredential(), but we opt to plumb the tokens
244-
// through our CredentialsProvider interface, at least for now.
245-
export class GoogleCredentialsProvider implements CredentialsProvider {
246-
constructor(private authClient: GoogleAuthClient) {}
247-
248-
getToken(forceRefresh: boolean): Promise<Token | null> {
249-
return new Promise<Token | null>((resolve, reject) => {
250-
// TODO(b/32935141): ideally this would be declared as an extern
251-
this.authClient['getAccessToken'](
252-
(error: AnyJs, tokenLiteral: string) => {
253-
if (error) {
254-
reject(error);
255-
} else {
256-
resolve(new OAuthToken(tokenLiteral, User.GOOGLE_CREDENTIALS));
257-
}
258-
}
259-
);
260-
});
261-
}
262-
263-
// NOTE: A google-auth-library client represents an immutable "user", so
264-
// once we fire the initial event, it'll never change.
265-
setUserChangeListener(listener: UserListener): void {
266-
// Fire with initial uid.
267-
listener(User.GOOGLE_CREDENTIALS);
268-
}
269-
270-
removeUserChangeListener(): void {}
271-
}
272-
273-
/**
274-
* Very incomplete typing for an auth client from
275-
* https://github.com/google/google-auth-library-nodejs/
276-
*/
277-
export interface GoogleAuthClient {
278-
getAccessToken(callback: (error?: Error, token?: string) => void): void;
279-
}
280235
// TODO(b/32935141): Ideally gapi type would be declared as an extern
281236
// tslint:disable-next-line:no-any
282237
export type Gapi = any;
@@ -348,9 +303,6 @@ export function makeCredentialsProvider(credentials?: CredentialsSettings) {
348303
}
349304

350305
switch (credentials.type) {
351-
case 'google-auth':
352-
return new GoogleCredentialsProvider(credentials.client);
353-
354306
case 'gapi':
355307
return new FirstPartyCredentialsProvider(
356308
credentials.client,

packages/firestore/src/local/garbage_source.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export interface GarbageSource {
3737
* This can be used by garbage collectors to double-check if a key exists in
3838
* this collection when it was released elsewhere.
3939
*
40-
* PORTING NODE: This is used in contexts where PersistenceTransaction is
40+
* PORTING NOTE: This is used in contexts where PersistenceTransaction is
4141
* known not to be needed, in this case we just pass in null. Therefore
4242
* any implementations must gaurd against null values.
4343
*/

packages/firestore/src/platform/platform.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ProtoByteString } from '../core/types';
1919
import { Connection } from '../remote/connection';
2020
import { JsonProtoSerializer } from '../remote/serializer';
2121
import { fail } from '../util/assert';
22+
import { AnyJs } from '../util/misc';
2223

2324
/**
2425
* Provides a common interface to load anything platform dependent, e.g.
@@ -30,6 +31,9 @@ export interface Platform {
3031
loadConnection(databaseInfo: DatabaseInfo): Promise<Connection>;
3132
newSerializer(databaseId: DatabaseId): JsonProtoSerializer;
3233

34+
/** Formats an object as a JSON string, suitable for logging. */
35+
formatJSON(value: AnyJs): string;
36+
3337
/** Converts a Base64 encoded string to a binary string. */
3438
atob(encoded: string): string;
3539

packages/firestore/src/platform_browser/browser_platform.ts

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Connection } from '../remote/connection';
2020
import { JsonProtoSerializer } from '../remote/serializer';
2121

2222
import { WebChannelConnection } from './webchannel_connection';
23+
import { AnyJs } from '../util/misc';
2324

2425
export class BrowserPlatform implements Platform {
2526
readonly base64Available: boolean;
@@ -38,6 +39,10 @@ export class BrowserPlatform implements Platform {
3839
return new JsonProtoSerializer(databaseId, { useProto3Json: true });
3940
}
4041

42+
formatJSON(value: AnyJs): string {
43+
return JSON.stringify(value);
44+
}
45+
4146
atob(encoded: string): string {
4247
return atob(encoded);
4348
}

packages/firestore/src/platform_browser/webchannel_connection.ts

+23-27
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,15 @@ import { Rejecter, Resolver } from '../util/promise';
3838

3939
const LOG_TAG = 'Connection';
4040

41+
const RPC_STREAM_SERVICE = 'google.firestore.v1beta1.Firestore';
4142
const RPC_URL_VERSION = 'v1beta1';
4243

44+
/** Maps RPC names to the corresponding REST endpoint name. */
45+
const RPC_NAME_REST_MAPPING = {
46+
BatchGetDocuments: 'batchGet',
47+
Commit: 'commit'
48+
};
49+
4350
// TODO(b/38203344): The SDK_VERSION is set independently from Firebase because
4451
// we are doing out-of-band releases. Once we release as part of Firebase, we
4552
// should use the Firebase version instead.
@@ -52,24 +59,6 @@ export class WebChannelConnection implements Connection {
5259
private readonly baseUrl: string;
5360
private readonly pool: XhrIoPool;
5461

55-
/**
56-
* Mapping from RPC name to service providing the RPC.
57-
* For streaming RPCs only.
58-
*/
59-
private static RPC_STREAM_SERVICE_MAPPING: { [rpc: string]: string } = {
60-
Write: 'google.firestore.v1beta1.Firestore',
61-
Listen: 'google.firestore.v1beta1.Firestore'
62-
};
63-
64-
/**
65-
* Mapping from RPC name to actual RPC name in URLs.
66-
* For streaming RPCs only.
67-
*/
68-
private static RPC_STREAM_NAME_MAPPING: { [rpc: string]: string } = {
69-
Write: 'Write',
70-
Listen: 'Listen'
71-
};
72-
7362
constructor(info: DatabaseInfo) {
7463
this.databaseId = info.databaseId;
7564
this.pool = new XhrIoPool();
@@ -100,7 +89,7 @@ export class WebChannelConnection implements Connection {
10089
`databases/${this.databaseId.database}`;
10190
}
10291

103-
invoke(rpcName: string, request: any, token: Token | null): Promise<any> {
92+
invokeRPC(rpcName: string, request: any, token: Token | null): Promise<any> {
10493
const url = this.makeUrl(rpcName);
10594

10695
return new Promise((resolve: Resolver<any>, reject: Rejecter) => {
@@ -178,18 +167,23 @@ export class WebChannelConnection implements Connection {
178167
});
179168
}
180169

170+
invokeStreamingRPC(
171+
rpcName: string,
172+
request: any,
173+
token: Token | null
174+
): Promise<any[]> {
175+
// The REST API automatically aggregates all of the streamed results, so we
176+
// can just use the normal invoke() method.
177+
return this.invokeRPC(rpcName, request, token);
178+
}
179+
181180
openStream(rpcName: string, token: Token | null): Stream<any, any> {
182-
const rpcService = WebChannelConnection.RPC_STREAM_SERVICE_MAPPING[rpcName];
183-
const rpcUrlName = WebChannelConnection.RPC_STREAM_NAME_MAPPING[rpcName];
184-
if (!rpcService || !rpcUrlName) {
185-
fail('Unknown RPC name: ' + rpcName);
186-
}
187181
const urlParts = [
188182
this.baseUrl,
189183
'/',
190-
rpcService,
184+
RPC_STREAM_SERVICE,
191185
'/',
192-
rpcUrlName,
186+
rpcName,
193187
'/channel'
194188
];
195189
const webchannelTransport = createWebChannelTransport();
@@ -343,6 +337,8 @@ export class WebChannelConnection implements Connection {
343337

344338
// visible for testing
345339
makeUrl(rpcName: string): string {
340+
const urlRpcName = RPC_NAME_REST_MAPPING[rpcName];
341+
assert(urlRpcName !== undefined, 'Unknown REST mapping for: ' + rpcName);
346342
const url = [this.baseUrl, '/', RPC_URL_VERSION];
347343
url.push('/projects/');
348344
url.push(this.databaseId.projectId);
@@ -352,7 +348,7 @@ export class WebChannelConnection implements Connection {
352348
url.push('/documents');
353349

354350
url.push(':');
355-
url.push(rpcName);
351+
url.push(urlRpcName);
356352
return url.join('');
357353
}
358354
}

packages/firestore/src/platform_mock_node/mock_init.ts

-27
This file was deleted.

0 commit comments

Comments
 (0)