From 7f9b3d96b26d78e95b857694a3e41e15ef0b3f04 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 4 Aug 2020 14:17:32 -0700 Subject: [PATCH] Stop using WebChannelConnection in Lite SDK (#3482) --- config/webpack.test.js | 3 +- .../firestore_lite_Tests__Emulator_.xml | 1 + packages/firestore/karma.conf.js | 1 + packages/firestore/package.json | 3 +- packages/firestore/rollup.config.lite.js | 9 +- packages/firestore/scripts/run-tests.js | 2 +- packages/firestore/scripts/run-tests.ts | 5 + packages/firestore/src/platform/base64.ts | 32 +--- .../platform/browser/webchannel_connection.ts | 110 ++----------- .../src/platform/browser_lite/base64.ts | 18 ++ .../src/platform/browser_lite/connection.ts | 27 +++ .../src/platform/browser_lite/dom.ts | 18 ++ .../platform/browser_lite/fetch_connection.ts | 80 +++++++++ .../src/platform/browser_lite/format_json.ts | 18 ++ .../src/platform/browser_lite/random_bytes.ts | 18 ++ .../src/platform/browser_lite/serializer.ts | 23 +++ packages/firestore/src/platform/connection.ts | 26 +-- packages/firestore/src/platform/dom.ts | 23 +-- .../firestore/src/platform/format_json.ts | 17 +- .../src/platform/node/grpc_connection.ts | 29 ++-- .../src/platform/node_lite/base64.ts | 18 ++ .../src/platform/node_lite/connection.ts | 32 ++++ .../firestore/src/platform/node_lite/dom.ts | 18 ++ .../src/platform/node_lite/format_json.ts | 18 ++ .../src/platform/node_lite/random_bytes.ts | 18 ++ .../src/platform/node_lite/serializer.ts | 24 +++ .../firestore/src/platform/random_bytes.ts | 17 +- .../firestore/src/platform/rn_lite/base64.ts | 18 ++ .../src/platform/rn_lite/connection.ts | 18 ++ .../firestore/src/platform/rn_lite/dom.ts | 18 ++ .../src/platform/rn_lite/format_json.ts | 18 ++ .../src/platform/rn_lite/random_bytes.ts | 18 ++ .../src/platform/rn_lite/serializer.ts | 18 ++ packages/firestore/src/platform/serializer.ts | 17 +- packages/firestore/src/remote/connection.ts | 4 + packages/firestore/src/remote/datastore.ts | 41 ++--- .../firestore/src/remote/rest_connection.ts | 155 ++++++++++++++++++ packages/firestore/src/remote/rpc_error.ts | 11 +- .../test/integration/api/database.test.ts | 2 +- .../integration/browser/webchannel.test.ts | 21 --- .../integration/util/events_accumulator.ts | 2 +- .../test/unit/remote/rest_connection.test.ts | 120 ++++++++++++++ 42 files changed, 827 insertions(+), 262 deletions(-) create mode 100644 packages/firestore/src/platform/browser_lite/base64.ts create mode 100644 packages/firestore/src/platform/browser_lite/connection.ts create mode 100644 packages/firestore/src/platform/browser_lite/dom.ts create mode 100644 packages/firestore/src/platform/browser_lite/fetch_connection.ts create mode 100644 packages/firestore/src/platform/browser_lite/format_json.ts create mode 100644 packages/firestore/src/platform/browser_lite/random_bytes.ts create mode 100644 packages/firestore/src/platform/browser_lite/serializer.ts create mode 100644 packages/firestore/src/platform/node_lite/base64.ts create mode 100644 packages/firestore/src/platform/node_lite/connection.ts create mode 100644 packages/firestore/src/platform/node_lite/dom.ts create mode 100644 packages/firestore/src/platform/node_lite/format_json.ts create mode 100644 packages/firestore/src/platform/node_lite/random_bytes.ts create mode 100644 packages/firestore/src/platform/node_lite/serializer.ts create mode 100644 packages/firestore/src/platform/rn_lite/base64.ts create mode 100644 packages/firestore/src/platform/rn_lite/connection.ts create mode 100644 packages/firestore/src/platform/rn_lite/dom.ts create mode 100644 packages/firestore/src/platform/rn_lite/format_json.ts create mode 100644 packages/firestore/src/platform/rn_lite/random_bytes.ts create mode 100644 packages/firestore/src/platform/rn_lite/serializer.ts create mode 100644 packages/firestore/src/remote/rest_connection.ts create mode 100644 packages/firestore/test/unit/remote/rest_connection.test.ts diff --git a/config/webpack.test.js b/config/webpack.test.js index 8094079e1e2..5da3c423b29 100644 --- a/config/webpack.test.js +++ b/config/webpack.test.js @@ -86,9 +86,10 @@ module.exports = { new webpack.NormalModuleReplacementPlugin( FIRESTORE_PLATFORM_RE, resource => { + const targetPlatform = process.env.TEST_PLATFORM || 'browser'; resource.request = resource.request.replace( FIRESTORE_PLATFORM_RE, - '$1/platform/browser/$2.ts' + `$1/platform/${targetPlatform}/$2.ts` ); } ), diff --git a/packages/firestore/.idea/runConfigurations/firestore_lite_Tests__Emulator_.xml b/packages/firestore/.idea/runConfigurations/firestore_lite_Tests__Emulator_.xml index 0d03257c114..49fe37e3a8f 100644 --- a/packages/firestore/.idea/runConfigurations/firestore_lite_Tests__Emulator_.xml +++ b/packages/firestore/.idea/runConfigurations/firestore_lite_Tests__Emulator_.xml @@ -9,6 +9,7 @@ + bdd --require ts-node/register/type-check --require lite/index.ts --timeout 5000 diff --git a/packages/firestore/karma.conf.js b/packages/firestore/karma.conf.js index 3e301c94245..5c32e76f546 100644 --- a/packages/firestore/karma.conf.js +++ b/packages/firestore/karma.conf.js @@ -55,6 +55,7 @@ function getTestFiles(argv) { } else if (argv.integration) { return [legcayIntegrationTests]; } else if (argv.lite) { + process.env.TEST_PLATFORM = 'browser_lite'; return [liteIntegrationTests]; } else if (argv.exp) { return [expIntegrationTests]; diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 74e2bb8c75d..d9942ea7528 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -24,7 +24,7 @@ "gendeps:exp": "../../scripts/exp/extract-deps.sh --types ./exp-types/index.d.ts --bundle ./dist/exp/tmp.js --output ./exp/dependencies.json", "pregendeps:lite": "node scripts/build-bundle.js --input ./lite/index.ts --output ./dist/lite/tmp.js", "gendeps:lite": "../../scripts/exp/extract-deps.sh --types ./lite-types/index.d.ts --bundle ./dist/lite/tmp.js --output ./lite/dependencies.json", - "test:lite": "node ./scripts/run-tests.js --emulator --main=lite/index.ts 'lite/test/**/*.test.ts'", + "test:lite": "node ./scripts/run-tests.js --emulator --platform node_lite --main=lite/index.ts 'lite/test/**/*.test.ts'", "test:lite:browser": "karma start --single-run --lite", "test:lite:browser:debug": "karma start --single-run --lite --auto-watch", "test:exp": "node ./scripts/run-tests.js --emulator --main=exp/index.ts test/integration/api/*.test.ts", @@ -63,6 +63,7 @@ "@firebase/webchannel-wrapper": "0.2.41", "@grpc/grpc-js": "^1.0.0", "@grpc/proto-loader": "^0.5.0", + "node-fetch": "2.6.0", "tslib": "^1.11.1" }, "peerDependencies": { diff --git a/packages/firestore/rollup.config.lite.js b/packages/firestore/rollup.config.lite.js index bdf76719242..a808eb040d1 100644 --- a/packages/firestore/rollup.config.lite.js +++ b/packages/firestore/rollup.config.lite.js @@ -71,7 +71,7 @@ const allBuilds = [ format: 'es', sourcemap: true }, - plugins: [alias(util.generateAliasConfig('node')), ...nodePlugins], + plugins: [alias(util.generateAliasConfig('node_lite')), ...nodePlugins], external: util.resolveNodeExterns, treeshake: { moduleSideEffects: false @@ -111,7 +111,10 @@ const allBuilds = [ format: 'es', sourcemap: true }, - plugins: [alias(util.generateAliasConfig('browser')), ...browserPlugins], + plugins: [ + alias(util.generateAliasConfig('browser_lite')), + ...browserPlugins + ], external: util.resolveBrowserExterns, treeshake: { moduleSideEffects: false @@ -125,7 +128,7 @@ const allBuilds = [ format: 'es', sourcemap: true }, - plugins: [alias(util.generateAliasConfig('rn')), ...browserPlugins], + plugins: [alias(util.generateAliasConfig('rn_lite')), ...browserPlugins], external: util.resolveBrowserExterns, treeshake: { moduleSideEffects: false diff --git a/packages/firestore/scripts/run-tests.js b/packages/firestore/scripts/run-tests.js index 4ef3286aceb..3bee79d139e 100644 --- a/packages/firestore/scripts/run-tests.js +++ b/packages/firestore/scripts/run-tests.js @@ -14,4 +14,4 @@ * 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. - */exports.__esModule=true;var yargs=require("yargs");var path_1=require("path");var child_process_promise_1=require("child-process-promise");var argv=yargs.options({main:{type:"string",demandOption:true},emulator:{type:"boolean"},persistence:{type:"boolean"}}).argv;var nyc=path_1.resolve(__dirname,"../../../node_modules/.bin/nyc");var mocha=path_1.resolve(__dirname,"../../../node_modules/.bin/mocha");process.env.TS_NODE_CACHE="NO";process.env.TS_NODE_COMPILER_OPTIONS='{"module":"commonjs"}';var args=[mocha,"--require","ts-node/register","--require",argv.main,"--config","../../config/mocharc.node.js"];if(argv.emulator){process.env.FIRESTORE_EMULATOR_PORT="8080";process.env.FIRESTORE_EMULATOR_PROJECT_ID="test-emulator"}if(argv.persistence){process.env.USE_MOCK_PERSISTENCE="YES";args.push("--require","test/util/node_persistence.ts")}args=args.concat(argv._);var childProcess=child_process_promise_1.spawn(nyc,args,{stdio:"inherit",cwd:process.cwd()}).childProcess;process.once("exit",(function(){return childProcess.kill()}));process.once("SIGINT",(function(){return childProcess.kill("SIGINT")}));process.once("SIGTERM",(function(){return childProcess.kill("SIGTERM")})); \ No newline at end of file + */exports.__esModule=true;var yargs=require("yargs");var path_1=require("path");var child_process_promise_1=require("child-process-promise");var argv=yargs.options({main:{type:"string",demandOption:true},platform:{type:"string",default:"node"},emulator:{type:"boolean"},persistence:{type:"boolean"}}).argv;var nyc=path_1.resolve(__dirname,"../../../node_modules/.bin/nyc");var mocha=path_1.resolve(__dirname,"../../../node_modules/.bin/mocha");process.env.TS_NODE_CACHE="NO";process.env.TS_NODE_COMPILER_OPTIONS='{"module":"commonjs"}';process.env.TEST_PLATFORM=argv.platform;var args=[mocha,"--require","ts-node/register","--require",argv.main,"--config","../../config/mocharc.node.js"];if(argv.emulator){process.env.FIRESTORE_EMULATOR_PORT="8080";process.env.FIRESTORE_EMULATOR_PROJECT_ID="test-emulator"}if(argv.persistence){process.env.USE_MOCK_PERSISTENCE="YES";args.push("--require","test/util/node_persistence.ts")}args=args.concat(argv._);var childProcess=child_process_promise_1.spawn(nyc,args,{stdio:"inherit",cwd:process.cwd()}).childProcess;process.once("exit",(function(){return childProcess.kill()}));process.once("SIGINT",(function(){return childProcess.kill("SIGINT")}));process.once("SIGTERM",(function(){return childProcess.kill("SIGTERM")})); \ No newline at end of file diff --git a/packages/firestore/scripts/run-tests.ts b/packages/firestore/scripts/run-tests.ts index 42013c78969..8cf75e3aae0 100644 --- a/packages/firestore/scripts/run-tests.ts +++ b/packages/firestore/scripts/run-tests.ts @@ -24,6 +24,10 @@ const argv = yargs.options({ type: 'string', demandOption: true }, + platform: { + type: 'string', + default: 'node' + }, emulator: { type: 'boolean' }, @@ -37,6 +41,7 @@ const mocha = resolve(__dirname, '../../../node_modules/.bin/mocha'); process.env.TS_NODE_CACHE = 'NO'; process.env.TS_NODE_COMPILER_OPTIONS = '{"module":"commonjs"}'; +process.env.TEST_PLATFORM = argv.platform; let args = [ mocha, diff --git a/packages/firestore/src/platform/base64.ts b/packages/firestore/src/platform/base64.ts index c40155c8d5b..b794158d909 100644 --- a/packages/firestore/src/platform/base64.ts +++ b/packages/firestore/src/platform/base64.ts @@ -15,41 +15,21 @@ * limitations under the License. */ -import { isNode, isReactNative } from '@firebase/util'; - -import * as node from './node/base64'; -import * as rn from './rn/base64'; -import * as browser from './browser/base64'; +// This file is only used under ts-node. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const platform = require(`./${process.env.TEST_PLATFORM ?? 'node'}/base64`); /** Converts a Base64 encoded string to a binary string. */ export function decodeBase64(encoded: string): string { - if (isNode()) { - return node.decodeBase64(encoded); - } else if (isReactNative()) { - return rn.decodeBase64(encoded); - } else { - return browser.decodeBase64(encoded); - } + return platform.decodeBase64(encoded); } /** Converts a binary string to a Base64 encoded string. */ export function encodeBase64(raw: string): string { - if (isNode()) { - return node.encodeBase64(raw); - } else if (isReactNative()) { - return rn.encodeBase64(raw); - } else { - return browser.encodeBase64(raw); - } + return platform.encodeBase64(raw); } /** True if and only if the Base64 conversion functions are available. */ export function isBase64Available(): boolean { - if (isNode()) { - return node.isBase64Available(); - } else if (isReactNative()) { - return rn.isBase64Available(); - } else { - return browser.isBase64Available(); - } + return platform.isBase64Available(); } diff --git a/packages/firestore/src/platform/browser/webchannel_connection.ts b/packages/firestore/src/platform/browser/webchannel_connection.ts index 5e3256ec42e..0c0bb901863 100644 --- a/packages/firestore/src/platform/browser/webchannel_connection.ts +++ b/packages/firestore/src/platform/browser/webchannel_connection.ts @@ -35,79 +35,40 @@ import { } from '@firebase/util'; import { Token } from '../../api/credentials'; -import { DatabaseId, DatabaseInfo } from '../../core/database_info'; -import { SDK_VERSION } from '../../core/version'; -import { Connection, Stream } from '../../remote/connection'; +import { DatabaseInfo } from '../../core/database_info'; +import { Stream } from '../../remote/connection'; import { mapCodeFromRpcStatus, mapCodeFromHttpResponseErrorStatus } from '../../remote/rpc_error'; import { StreamBridge } from '../../remote/stream_bridge'; -import { debugAssert, fail, hardAssert } from '../../util/assert'; +import { fail, hardAssert } from '../../util/assert'; import { Code, FirestoreError } from '../../util/error'; import { logDebug, logWarn } from '../../util/log'; -import { Indexable } from '../../util/misc'; import { Rejecter, Resolver } from '../../util/promise'; import { StringMap } from '../../util/types'; +import { RestConnection } from '../../remote/rest_connection'; const LOG_TAG = 'Connection'; const RPC_STREAM_SERVICE = 'google.firestore.v1.Firestore'; -const RPC_URL_VERSION = 'v1'; - -/** - * Maps RPC names to the corresponding REST endpoint name. - * Uses Object Literal notation to avoid renaming. - */ -const RPC_NAME_REST_MAPPING: { [key: string]: string } = {}; -RPC_NAME_REST_MAPPING['BatchGetDocuments'] = 'batchGet'; -RPC_NAME_REST_MAPPING['Commit'] = 'commit'; -RPC_NAME_REST_MAPPING['RunQuery'] = 'runQuery'; - -// TODO(b/38203344): The SDK_VERSION is set independently from Firebase because -// we are doing out-of-band releases. Once we release as part of Firebase, we -// should use the Firebase version instead. -const X_GOOG_API_CLIENT_VALUE = 'gl-js/ fire/' + SDK_VERSION; const XHR_TIMEOUT_SECS = 15; -export class WebChannelConnection implements Connection { - private readonly databaseId: DatabaseId; - private readonly baseUrl: string; +export class WebChannelConnection extends RestConnection { private readonly forceLongPolling: boolean; constructor(info: DatabaseInfo) { - this.databaseId = info.databaseId; - const proto = info.ssl ? 'https' : 'http'; - this.baseUrl = proto + '://' + info.host; + super(info); this.forceLongPolling = info.forceLongPolling; } - /** - * Modifies the headers for a request, adding any authorization token if - * present and any additional headers for the request. - */ - private modifyHeadersForRequest( - headers: StringMap, - token: Token | null - ): void { - if (token) { - for (const header in token.authHeaders) { - if (token.authHeaders.hasOwnProperty(header)) { - headers[header] = token.authHeaders[header]; - } - } - } - headers['X-Goog-Api-Client'] = X_GOOG_API_CLIENT_VALUE; - } - - invokeRPC( + protected performRPCRequest( rpcName: string, - request: Req, - token: Token | null + url: string, + headers: StringMap, + body: Req ): Promise { - const url = this.makeUrl(rpcName); - return new Promise((resolve: Resolver, reject: Rejecter) => { const xhr = new XhrIo(); xhr.listenOnce(EventType.COMPLETE, () => { @@ -161,7 +122,6 @@ export class WebChannelConnection implements Connection { } else { // If we received an HTTP_ERROR but there's no status code, // it's most probably a connection issue - logDebug(LOG_TAG, 'RPC "' + rpcName + '" failed'); reject( new FirestoreError(Code.UNAVAILABLE, 'Connection failed.') ); @@ -184,37 +144,11 @@ export class WebChannelConnection implements Connection { } }); - // The database field is already encoded in URL. Specifying it again in - // the body is not necessary in production, and will cause duplicate field - // errors in the Firestore Emulator. Let's remove it. - const jsonObj = ({ ...request } as unknown) as Indexable; - delete jsonObj.database; - - const requestString = JSON.stringify(jsonObj); - logDebug(LOG_TAG, 'XHR sending: ', url + ' ' + requestString); - // Content-Type: text/plain will avoid preflight requests which might - // mess with CORS and redirects by proxies. If we add custom headers - // we will need to change this code to potentially use the - // $httpOverwrite parameter supported by ESF to avoid - // triggering preflight requests. - const headers: StringMap = { 'Content-Type': 'text/plain' }; - - this.modifyHeadersForRequest(headers, token); - + const requestString = JSON.stringify(body); xhr.send(url, 'POST', requestString, headers, XHR_TIMEOUT_SECS); }); } - invokeStreamingRPC( - rpcName: string, - request: Req, - token: Token | null - ): Promise { - // The REST API automatically aggregates all of the streamed results, so we - // can just use the normal invoke() method. - return this.invokeRPC(rpcName, request, token); - } - openStream( rpcName: string, token: Token | null @@ -283,7 +217,7 @@ export class WebChannelConnection implements Connection { } const url = urlParts.join(''); - logDebug(LOG_TAG, 'Creating WebChannel: ' + url + ' ' + request); + logDebug(LOG_TAG, 'Creating WebChannel: ' + url, request); const channel = webchannelTransport.createWebChannel(url, request); // WebChannel supports sending the first message with the handshake - saving @@ -420,24 +354,4 @@ export class WebChannelConnection implements Connection { }, 0); return streamBridge; } - - // visible for testing - makeUrl(rpcName: string): string { - const urlRpcName = RPC_NAME_REST_MAPPING[rpcName]; - debugAssert( - urlRpcName !== undefined, - 'Unknown REST mapping for: ' + rpcName - ); - return ( - this.baseUrl + - '/' + - RPC_URL_VERSION + - '/projects/' + - this.databaseId.projectId + - '/databases/' + - this.databaseId.database + - '/documents:' + - urlRpcName - ); - } } diff --git a/packages/firestore/src/platform/browser_lite/base64.ts b/packages/firestore/src/platform/browser_lite/base64.ts new file mode 100644 index 00000000000..9214f8da36b --- /dev/null +++ b/packages/firestore/src/platform/browser_lite/base64.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../browser/base64'; diff --git a/packages/firestore/src/platform/browser_lite/connection.ts b/packages/firestore/src/platform/browser_lite/connection.ts new file mode 100644 index 00000000000..1aca0a8ee9b --- /dev/null +++ b/packages/firestore/src/platform/browser_lite/connection.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 { DatabaseInfo } from '../../core/database_info'; +import { Connection } from '../../remote/connection'; +import { FetchConnection } from './fetch_connection'; + +export { newConnectivityMonitor } from '../browser/connection'; + +/** Initializes the HTTP connection for the REST API. */ +export function newConnection(databaseInfo: DatabaseInfo): Promise { + return Promise.resolve(new FetchConnection(databaseInfo, fetch.bind(null))); +} diff --git a/packages/firestore/src/platform/browser_lite/dom.ts b/packages/firestore/src/platform/browser_lite/dom.ts new file mode 100644 index 00000000000..7704d7a790b --- /dev/null +++ b/packages/firestore/src/platform/browser_lite/dom.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../browser/dom'; diff --git a/packages/firestore/src/platform/browser_lite/fetch_connection.ts b/packages/firestore/src/platform/browser_lite/fetch_connection.ts new file mode 100644 index 00000000000..d0644f1c7de --- /dev/null +++ b/packages/firestore/src/platform/browser_lite/fetch_connection.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 { Token } from '../../api/credentials'; +import { Stream } from '../../remote/connection'; +import { mapCodeFromHttpStatus } from '../../remote/rpc_error'; +import { FirestoreError } from '../../util/error'; +import { StringMap } from '../../util/types'; +import { RestConnection } from '../../remote/rest_connection'; +import { DatabaseInfo } from '../../core/database_info'; + +/** + * A Rest-based connection that relies on the native HTTP stack + * (e.g. `fetch` or a polyfill). + */ +export class FetchConnection extends RestConnection { + /** + * @param databaseInfo The connection info. + * @param fetchImpl `fetch` or a Polyfill that implements the fetch API. + */ + constructor( + databaseInfo: DatabaseInfo, + private readonly fetchImpl: typeof fetch + ) { + super(databaseInfo); + } + + openStream( + rpcName: string, + token: Token | null + ): Stream { + throw new Error('Not supported by FetchConnection'); + } + + protected async performRPCRequest( + rpcName: string, + url: string, + headers: StringMap, + body: Req + ): Promise { + const requestJson = JSON.stringify(body); + let response: Response; + + try { + response = await this.fetchImpl(url, { + method: 'POST', + headers, + body: requestJson + }); + } catch (err) { + throw new FirestoreError( + mapCodeFromHttpStatus(err.status), + 'Request failed with error: ' + err.statusText + ); + } + + if (!response.ok) { + throw new FirestoreError( + mapCodeFromHttpStatus(response.status), + 'Request failed with error: ' + response.statusText + ); + } + + return response.json(); + } +} diff --git a/packages/firestore/src/platform/browser_lite/format_json.ts b/packages/firestore/src/platform/browser_lite/format_json.ts new file mode 100644 index 00000000000..278b5dfbba1 --- /dev/null +++ b/packages/firestore/src/platform/browser_lite/format_json.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../browser/format_json'; diff --git a/packages/firestore/src/platform/browser_lite/random_bytes.ts b/packages/firestore/src/platform/browser_lite/random_bytes.ts new file mode 100644 index 00000000000..6270b257114 --- /dev/null +++ b/packages/firestore/src/platform/browser_lite/random_bytes.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../browser/random_bytes'; diff --git a/packages/firestore/src/platform/browser_lite/serializer.ts b/packages/firestore/src/platform/browser_lite/serializer.ts new file mode 100644 index 00000000000..ed490bf16d7 --- /dev/null +++ b/packages/firestore/src/platform/browser_lite/serializer.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 { JsonProtoSerializer } from '../../remote/serializer'; +import { DatabaseId } from '../../core/database_info'; + +export function newSerializer(databaseId: DatabaseId): JsonProtoSerializer { + return new JsonProtoSerializer(databaseId, /* useProto3Json= */ true); +} diff --git a/packages/firestore/src/platform/connection.ts b/packages/firestore/src/platform/connection.ts index 6b8148ccde5..bb50a94f008 100644 --- a/packages/firestore/src/platform/connection.ts +++ b/packages/firestore/src/platform/connection.ts @@ -14,31 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { isNode, isReactNative } from '@firebase/util'; -import * as node from './node/connection'; -import * as rn from './rn/connection'; -import * as browser from './browser/connection'; import { ConnectivityMonitor } from '../remote/connectivity_monitor'; import { DatabaseInfo } from '../core/database_info'; import { Connection } from '../remote/connection'; +// This file is only used under ts-node. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const platform = require(`./${process.env.TEST_PLATFORM ?? 'node'}/connection`); + export function newConnectivityMonitor(): ConnectivityMonitor { - if (isNode()) { - return node.newConnectivityMonitor(); - } else if (isReactNative()) { - return rn.newConnectivityMonitor(); - } else { - return browser.newConnectivityMonitor(); - } + return platform.newConnectivityMonitor(); } +// TODO(firestorexp): This doesn't need to return a Promise export function newConnection(databaseInfo: DatabaseInfo): Promise { - if (isNode()) { - return node.newConnection(databaseInfo); - } else if (isReactNative()) { - return rn.newConnection(databaseInfo); - } else { - return browser.newConnection(databaseInfo); - } + return platform.newConnection(databaseInfo); } diff --git a/packages/firestore/src/platform/dom.ts b/packages/firestore/src/platform/dom.ts index 33090224beb..4ad06156ac3 100644 --- a/packages/firestore/src/platform/dom.ts +++ b/packages/firestore/src/platform/dom.ts @@ -15,29 +15,16 @@ * limitations under the License. */ -import { isNode, isReactNative } from '@firebase/util'; -import * as node from './node/dom'; -import * as rn from './rn/dom'; -import * as browser from './browser/dom'; +// This file is only used under ts-node. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const platform = require(`./${process.env.TEST_PLATFORM ?? 'node'}/dom`); /** The Platform's 'window' implementation or null if not available. */ export function getWindow(): Window | null { - if (isNode()) { - return node.getWindow(); - } else if (isReactNative()) { - return rn.getWindow(); - } else { - return browser.getWindow(); - } + return platform.getWindow(); } /** The Platform's 'document' implementation or null if not available. */ export function getDocument(): Document | null { - if (isNode()) { - return node.getDocument(); - } else if (isReactNative()) { - return rn.getDocument(); - } else { - return browser.getDocument(); - } + return platform.getDocument(); } diff --git a/packages/firestore/src/platform/format_json.ts b/packages/firestore/src/platform/format_json.ts index 042fe5c00f2..e25fca4ad94 100644 --- a/packages/firestore/src/platform/format_json.ts +++ b/packages/firestore/src/platform/format_json.ts @@ -15,18 +15,13 @@ * limitations under the License. */ -import { isNode, isReactNative } from '@firebase/util'; -import * as node from './node/format_json'; -import * as rn from './rn/format_json'; -import * as browser from './browser/format_json'; +// This file is only used under ts-node. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const platform = require(`./${ + process.env.TEST_PLATFORM ?? 'node' +}/format_json`); /** Formats an object as a JSON string, suitable for logging. */ export function formatJSON(value: unknown): string { - if (isNode()) { - return node.formatJSON(value); - } else if (isReactNative()) { - return rn.formatJSON(value); - } else { - return browser.formatJSON(value); - } + return platform.formatJSON(value); } diff --git a/packages/firestore/src/platform/node/grpc_connection.ts b/packages/firestore/src/platform/node/grpc_connection.ts index 13bdf0f6bb4..46855192be1 100644 --- a/packages/firestore/src/platform/node/grpc_connection.ts +++ b/packages/firestore/src/platform/node/grpc_connection.ts @@ -46,10 +46,7 @@ const LOG_TAG = 'Connection'; // should use the Firebase version instead. const X_GOOG_API_CLIENT_VALUE = `gl-node/${process.versions.node} fire/${SDK_VERSION} grpc/${grpcVersion}`; -function createMetadata( - databaseInfo: DatabaseInfo, - token: Token | null -): Metadata { +function createMetadata(databasePath: string, token: Token | null): Metadata { hardAssert( token === null || token.type === 'OAuth', 'If provided, token must be OAuth' @@ -66,11 +63,7 @@ function createMetadata( metadata.set('x-goog-api-client', X_GOOG_API_CLIENT_VALUE); // This header is used to improve routing and project isolation by the // backend. - metadata.set( - 'google-cloud-resource-prefix', - `projects/${databaseInfo.databaseId.projectId}/` + - `databases/${databaseInfo.databaseId.database}` - ); + metadata.set('google-cloud-resource-prefix', databasePath); return metadata; } @@ -83,8 +76,9 @@ type GeneratedGrpcStub = any; * A Connection implemented by GRPC-Node. */ export class GrpcConnection implements Connection { + private readonly databasePath: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any - private firestore: any; + private readonly firestore: any; // We cache stubs for the most-recently-used token. private cachedStub: GeneratedGrpcStub | null = null; @@ -92,6 +86,7 @@ export class GrpcConnection implements Connection { constructor(protos: GrpcObject, private databaseInfo: DatabaseInfo) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.firestore = (protos as any)['google']['firestore']['v1']; + this.databasePath = `projects/${databaseInfo.databaseId.projectId}/databases/${databaseInfo.databaseId.database}`; } private ensureActiveStub(): GeneratedGrpcStub { @@ -110,16 +105,18 @@ export class GrpcConnection implements Connection { invokeRPC( rpcName: string, + path: string, request: Req, token: Token | null ): Promise { const stub = this.ensureActiveStub(); - const metadata = createMetadata(this.databaseInfo, token); + const metadata = createMetadata(this.databasePath, token); + const jsonRequest = { database: this.databasePath, ...request }; return nodePromise((callback: NodeCallback) => { logDebug(LOG_TAG, `RPC '${rpcName}' invoked with request:`, request); return stub[rpcName]( - request, + jsonRequest, metadata, (grpcError?: ServiceError, value?: Resp) => { if (grpcError) { @@ -145,6 +142,7 @@ export class GrpcConnection implements Connection { invokeStreamingRPC( rpcName: string, + path: string, request: Req, token: Token | null ): Promise { @@ -157,8 +155,9 @@ export class GrpcConnection implements Connection { request ); const stub = this.ensureActiveStub(); - const metadata = createMetadata(this.databaseInfo, token); - const stream = stub[rpcName](request, metadata); + const metadata = createMetadata(this.databasePath, token); + const jsonRequest = { ...request, database: this.databasePath }; + const stream = stub[rpcName](jsonRequest, metadata); stream.on('data', (response: Resp) => { logDebug(LOG_TAG, `RPC ${rpcName} received result:`, response); results.push(response); @@ -182,7 +181,7 @@ export class GrpcConnection implements Connection { token: Token | null ): Stream { const stub = this.ensureActiveStub(); - const metadata = createMetadata(this.databaseInfo, token); + const metadata = createMetadata(this.databasePath, token); const grpcStream = stub[rpcName](metadata); let closed = false; diff --git a/packages/firestore/src/platform/node_lite/base64.ts b/packages/firestore/src/platform/node_lite/base64.ts new file mode 100644 index 00000000000..6f07d2a6591 --- /dev/null +++ b/packages/firestore/src/platform/node_lite/base64.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../node/base64'; diff --git a/packages/firestore/src/platform/node_lite/connection.ts b/packages/firestore/src/platform/node_lite/connection.ts new file mode 100644 index 00000000000..8063ff8e5f9 --- /dev/null +++ b/packages/firestore/src/platform/node_lite/connection.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 nodeFetch from 'node-fetch'; + +import { FetchConnection } from '../browser_lite/fetch_connection'; +import { DatabaseInfo } from '../../core/database_info'; +import { Connection } from '../../remote/connection'; + +export { newConnectivityMonitor } from '../browser/connection'; + +/** Initializes the HTTP connection for the REST API. */ +export function newConnection(databaseInfo: DatabaseInfo): Promise { + // node-fetch is meant to be API compatible with `fetch`, but its type don't + // match 100%. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return Promise.resolve(new FetchConnection(databaseInfo, nodeFetch as any)); +} diff --git a/packages/firestore/src/platform/node_lite/dom.ts b/packages/firestore/src/platform/node_lite/dom.ts new file mode 100644 index 00000000000..5b47065bde1 --- /dev/null +++ b/packages/firestore/src/platform/node_lite/dom.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../node/dom'; diff --git a/packages/firestore/src/platform/node_lite/format_json.ts b/packages/firestore/src/platform/node_lite/format_json.ts new file mode 100644 index 00000000000..391b770ff86 --- /dev/null +++ b/packages/firestore/src/platform/node_lite/format_json.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../node/format_json'; diff --git a/packages/firestore/src/platform/node_lite/random_bytes.ts b/packages/firestore/src/platform/node_lite/random_bytes.ts new file mode 100644 index 00000000000..5a6aa143df2 --- /dev/null +++ b/packages/firestore/src/platform/node_lite/random_bytes.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../node/random_bytes'; diff --git a/packages/firestore/src/platform/node_lite/serializer.ts b/packages/firestore/src/platform/node_lite/serializer.ts new file mode 100644 index 00000000000..c53bf2cbd7e --- /dev/null +++ b/packages/firestore/src/platform/node_lite/serializer.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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. + */ + +/** Return the Platform-specific serializer monitor. */ +import { JsonProtoSerializer } from '../../remote/serializer'; +import { DatabaseId } from '../../core/database_info'; + +export function newSerializer(databaseId: DatabaseId): JsonProtoSerializer { + return new JsonProtoSerializer(databaseId, /* useProto3Json= */ true); +} diff --git a/packages/firestore/src/platform/random_bytes.ts b/packages/firestore/src/platform/random_bytes.ts index 37edcc2d2dc..7db2c4cb7ba 100644 --- a/packages/firestore/src/platform/random_bytes.ts +++ b/packages/firestore/src/platform/random_bytes.ts @@ -15,10 +15,11 @@ * limitations under the License. */ -import { isNode, isReactNative } from '@firebase/util'; -import * as node from './node/random_bytes'; -import * as rn from './rn/random_bytes'; -import * as browser from './browser/random_bytes'; +// This file is only used under ts-node. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const platform = require(`./${ + process.env.TEST_PLATFORM ?? 'node' +}/random_bytes`); /** * Generates `nBytes` of random bytes. @@ -26,11 +27,5 @@ import * as browser from './browser/random_bytes'; * If `nBytes < 0` , an error will be thrown. */ export function randomBytes(nBytes: number): Uint8Array { - if (isNode()) { - return node.randomBytes(nBytes); - } else if (isReactNative()) { - return rn.randomBytes(nBytes); - } else { - return browser.randomBytes(nBytes); - } + return platform.randomBytes(nBytes); } diff --git a/packages/firestore/src/platform/rn_lite/base64.ts b/packages/firestore/src/platform/rn_lite/base64.ts new file mode 100644 index 00000000000..4dfcab364b1 --- /dev/null +++ b/packages/firestore/src/platform/rn_lite/base64.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../rn/base64'; diff --git a/packages/firestore/src/platform/rn_lite/connection.ts b/packages/firestore/src/platform/rn_lite/connection.ts new file mode 100644 index 00000000000..4ba28de1dce --- /dev/null +++ b/packages/firestore/src/platform/rn_lite/connection.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../browser_lite/connection'; diff --git a/packages/firestore/src/platform/rn_lite/dom.ts b/packages/firestore/src/platform/rn_lite/dom.ts new file mode 100644 index 00000000000..51a78621df9 --- /dev/null +++ b/packages/firestore/src/platform/rn_lite/dom.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../rn/dom'; diff --git a/packages/firestore/src/platform/rn_lite/format_json.ts b/packages/firestore/src/platform/rn_lite/format_json.ts new file mode 100644 index 00000000000..b1767402da3 --- /dev/null +++ b/packages/firestore/src/platform/rn_lite/format_json.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../rn/format_json'; diff --git a/packages/firestore/src/platform/rn_lite/random_bytes.ts b/packages/firestore/src/platform/rn_lite/random_bytes.ts new file mode 100644 index 00000000000..ac340911f15 --- /dev/null +++ b/packages/firestore/src/platform/rn_lite/random_bytes.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../rn/random_bytes'; diff --git a/packages/firestore/src/platform/rn_lite/serializer.ts b/packages/firestore/src/platform/rn_lite/serializer.ts new file mode 100644 index 00000000000..cdf6bf1dfb1 --- /dev/null +++ b/packages/firestore/src/platform/rn_lite/serializer.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 '../browser_lite/serializer'; diff --git a/packages/firestore/src/platform/serializer.ts b/packages/firestore/src/platform/serializer.ts index f7990dc4496..c2e416abdc1 100644 --- a/packages/firestore/src/platform/serializer.ts +++ b/packages/firestore/src/platform/serializer.ts @@ -14,20 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { isNode, isReactNative } from '@firebase/util'; -import * as node from './node/serializer'; -import * as rn from './rn/serializer'; -import * as browser from './browser/serializer'; import { DatabaseId } from '../core/database_info'; import { JsonProtoSerializer } from '../remote/serializer'; +// This file is only used under ts-node. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const platform = require(`./${process.env.TEST_PLATFORM ?? 'node'}/serializer`); + export function newSerializer(databaseId: DatabaseId): JsonProtoSerializer { - if (isNode()) { - return node.newSerializer(databaseId); - } else if (isReactNative()) { - return rn.newSerializer(databaseId); - } else { - return browser.newSerializer(databaseId); - } + return platform.newSerializer(databaseId); } diff --git a/packages/firestore/src/remote/connection.ts b/packages/firestore/src/remote/connection.ts index 3ad18ff8f21..adf4cd5a46b 100644 --- a/packages/firestore/src/remote/connection.ts +++ b/packages/firestore/src/remote/connection.ts @@ -38,12 +38,14 @@ export interface Connection { * representing the JSON to send. * * @param rpcName the name of the RPC to invoke + * @param path the path to invoke this RPC on * @param request the Raw JSON object encoding of the request message * @param token the Token to use for the RPC. * @return a Promise containing the JSON object encoding of the response */ invokeRPC( rpcName: string, + path: string, request: Req, token: Token | null ): Promise; @@ -54,6 +56,7 @@ export interface Connection { * completion and then returned as an array. * * @param rpcName the name of the RPC to invoke + * @param path the path to invoke this RPC on * @param request the Raw JSON object encoding of the request message * @param token the Token to use for the RPC. * @return a Promise containing an array with the JSON object encodings of the @@ -61,6 +64,7 @@ export interface Connection { */ invokeStreamingRPC( rpcName: string, + path: string, request: Req, token: Token | null ): Promise; diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index 96ec71c960d..e7f7f24b66a 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -82,12 +82,21 @@ class DatastoreImpl extends Datastore { } /** Gets an auth token and invokes the provided RPC. */ - invokeRPC(rpcName: string, request: Req): Promise { + invokeRPC( + rpcName: string, + path: string, + request: Req + ): Promise { this.verifyInitialized(); return this.credentials .getToken() .then(token => { - return this.connection.invokeRPC(rpcName, request, token); + return this.connection.invokeRPC( + rpcName, + path, + request, + token + ); }) .catch((error: FirestoreError) => { if (error.code === Code.UNAUTHENTICATED) { @@ -100,6 +109,7 @@ class DatastoreImpl extends Datastore { /** Gets an auth token and invokes the provided RPC with streamed results. */ invokeStreamingRPC( rpcName: string, + path: string, request: Req ): Promise { this.verifyInitialized(); @@ -108,6 +118,7 @@ class DatastoreImpl extends Datastore { .then(token => { return this.connection.invokeStreamingRPC( rpcName, + path, request, token ); @@ -139,11 +150,11 @@ export async function invokeCommitRpc( mutations: Mutation[] ): Promise { const datastoreImpl = debugCast(datastore, DatastoreImpl); - const params = { - database: getEncodedDatabaseId(datastoreImpl.serializer), + const path = getEncodedDatabaseId(datastoreImpl.serializer) + '/documents'; + const request = { writes: mutations.map(m => toMutation(datastoreImpl.serializer, m)) }; - await datastoreImpl.invokeRPC('Commit', params); + await datastoreImpl.invokeRPC('Commit', path, request); } export async function invokeBatchGetDocumentsRpc( @@ -151,14 +162,14 @@ export async function invokeBatchGetDocumentsRpc( keys: DocumentKey[] ): Promise { const datastoreImpl = debugCast(datastore, DatastoreImpl); - const params = { - database: getEncodedDatabaseId(datastoreImpl.serializer), + const path = getEncodedDatabaseId(datastoreImpl.serializer) + '/documents'; + const request = { documents: keys.map(k => toName(datastoreImpl.serializer, k)) }; const response = await datastoreImpl.invokeStreamingRPC< api.BatchGetDocumentsRequest, api.BatchGetDocumentsResponse - >('BatchGetDocuments', params); + >('BatchGetDocuments', path, request); const docs = new Map(); response.forEach(proto => { @@ -179,21 +190,11 @@ export async function invokeRunQueryRpc( query: Query ): Promise { const datastoreImpl = debugCast(datastore, DatastoreImpl); - const { structuredQuery, parent } = toQueryTarget( - datastoreImpl.serializer, - queryToTarget(query) - ); - const params = { - database: getEncodedDatabaseId(datastoreImpl.serializer), - parent, - structuredQuery - }; - + const request = toQueryTarget(datastoreImpl.serializer, queryToTarget(query)); const response = await datastoreImpl.invokeStreamingRPC< api.RunQueryRequest, api.RunQueryResponse - >('RunQuery', params); - + >('RunQuery', request.parent!, { structuredQuery: request.structuredQuery }); return ( response // Omit RunQueryResponses that only contain readTimes. diff --git a/packages/firestore/src/remote/rest_connection.ts b/packages/firestore/src/remote/rest_connection.ts new file mode 100644 index 00000000000..e39852707cc --- /dev/null +++ b/packages/firestore/src/remote/rest_connection.ts @@ -0,0 +1,155 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 { Token } from '../api/credentials'; +import { DatabaseId, DatabaseInfo } from '../core/database_info'; +import { SDK_VERSION } from '../../src/core/version'; +import { Connection, Stream } from './connection'; +import { logDebug, logWarn } from '../util/log'; +import { FirestoreError } from '../util/error'; +import { StringMap } from '../util/types'; +import { debugAssert } from '../util/assert'; + +const LOG_TAG = 'RestConnection'; + +/** + * Maps RPC names to the corresponding REST endpoint name. + * + * We use array notation to avoid mangling. + */ +const RPC_NAME_URL_MAPPING: StringMap = {}; + +RPC_NAME_URL_MAPPING['BatchGetDocuments'] = 'batchGet'; +RPC_NAME_URL_MAPPING['Commit'] = 'commit'; +RPC_NAME_URL_MAPPING['RunQuery'] = 'runQuery'; + +const RPC_URL_VERSION = 'v1'; +const X_GOOG_API_CLIENT_VALUE = 'gl-js/ fire/' + SDK_VERSION; + +/** + * Base class for all Rest-based connections to the backend (WebChannel and + * HTTP). + */ +export abstract class RestConnection implements Connection { + protected readonly databaseId: DatabaseId; + protected readonly baseUrl: string; + private readonly databaseRoot: string; + + constructor(private readonly databaseInfo: DatabaseInfo) { + this.databaseId = databaseInfo.databaseId; + const proto = databaseInfo.ssl ? 'https' : 'http'; + this.baseUrl = proto + '://' + databaseInfo.host; + this.databaseRoot = + 'projects/' + + this.databaseId.projectId + + '/databases/' + + this.databaseId.database + + '/documents'; + } + + invokeRPC( + rpcName: string, + path: string, + req: Req, + token: Token | null + ): Promise { + const url = this.makeUrl(rpcName, path); + logDebug(LOG_TAG, 'Sending: ', url, req); + + const headers = {}; + this.modifyHeadersForRequest(headers, token); + + return this.performRPCRequest(rpcName, url, headers, req).then( + response => { + logDebug(LOG_TAG, 'Received: ', response); + return response; + }, + (err: FirestoreError) => { + logWarn( + LOG_TAG, + `${rpcName} failed with error: `, + err, + 'url: ', + url, + 'request:', + req + ); + throw err; + } + ); + } + + invokeStreamingRPC( + rpcName: string, + path: string, + request: Req, + token: Token | null + ): Promise { + // The REST API automatically aggregates all of the streamed results, so we + // can just use the normal invoke() method. + return this.invokeRPC(rpcName, path, request, token); + } + + abstract openStream( + rpcName: string, + token: Token | null + ): Stream; + + /** + * Modifies the headers for a request, adding any authorization token if + * present and any additional headers for the request. + */ + protected modifyHeadersForRequest( + headers: StringMap, + token: Token | null + ): void { + headers['X-Goog-Api-Client'] = X_GOOG_API_CLIENT_VALUE; + + // Content-Type: text/plain will avoid preflight requests which might + // mess with CORS and redirects by proxies. If we add custom headers + // we will need to change this code to potentially use the $httpOverwrite + // parameter supported by ESF to avoid triggering preflight requests. + headers['Content-Type'] = 'text/plain'; + + if (token) { + for (const header in token.authHeaders) { + if (token.authHeaders.hasOwnProperty(header)) { + headers[header] = token.authHeaders[header]; + } + } + } + } + + /** + * Performs an RPC request using an implementation specific networking layer. + */ + protected abstract performRPCRequest( + rpcName: string, + url: string, + headers: StringMap, + body: Req + ): Promise; + + private makeUrl(rpcName: string, path: string): string { + const urlRpcName = RPC_NAME_URL_MAPPING[rpcName]; + debugAssert( + urlRpcName !== undefined, + 'Unknown REST mapping for: ' + rpcName + ); + return `${this.baseUrl}/${RPC_URL_VERSION}/${path}:${urlRpcName}`; + } +} diff --git a/packages/firestore/src/remote/rpc_error.ts b/packages/firestore/src/remote/rpc_error.ts index a5362a5907d..0d93662fb93 100644 --- a/packages/firestore/src/remote/rpc_error.ts +++ b/packages/firestore/src/remote/rpc_error.ts @@ -231,7 +231,12 @@ export function mapRpcCodeFromCode(code: Code | undefined): number { * @returns The equivalent Code. Unknown status codes are mapped to * Code.UNKNOWN. */ -export function mapCodeFromHttpStatus(status: number): Code { +export function mapCodeFromHttpStatus(status?: number): Code { + if (status === undefined) { + logError('RPC_ERROR', 'HTTP error has no status'); + return Code.UNKNOWN; + } + // The canonical error codes for Google APIs [1] specify mapping onto HTTP // status codes but the mapping is not bijective. In each case of ambiguity // this function chooses a primary error. @@ -243,9 +248,9 @@ export function mapCodeFromHttpStatus(status: number): Code { return Code.OK; case 400: // Bad Request - return Code.INVALID_ARGUMENT; + return Code.FAILED_PRECONDITION; // Other possibilities based on the forward mapping - // return Code.FAILED_PRECONDITION; + // return Code.INVALID_ARGUMENT; // return Code.OUT_OF_RANGE; case 401: // Unauthorized diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 2ba3f68d029..474bc25173f 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -20,7 +20,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as firestore from '@firebase/firestore-types'; import { expect, use } from 'chai'; -import { Deferred } from '../../util/promise'; +import { Deferred } from '@firebase/util'; import { EventsAccumulator } from '../util/events_accumulator'; import * as firebaseExport from '../util/firebase_export'; import { diff --git a/packages/firestore/test/integration/browser/webchannel.test.ts b/packages/firestore/test/integration/browser/webchannel.test.ts index 435b3b217dd..3d0f61f677b 100644 --- a/packages/firestore/test/integration/browser/webchannel.test.ts +++ b/packages/firestore/test/integration/browser/webchannel.test.ts @@ -16,7 +16,6 @@ */ import { expect } from 'chai'; -import { DatabaseId, DatabaseInfo } from '../../../src/core/database_info'; import { WebChannelConnection } from '../../../src/platform/browser/webchannel_connection'; import * as api from '../../../src/protos/firestore_proto_api'; import { DEFAULT_PROJECT_ID } from '../util/settings'; @@ -33,26 +32,6 @@ const describeFn = xdescribe; describeFn('WebChannel', () => { - describe('makeUrl', () => { - const info = new DatabaseInfo( - new DatabaseId('testproject'), - 'persistenceKey', - 'example.com', - /*ssl=*/ false, - /*forceLongPolling=*/ false - ); - const conn = new WebChannelConnection(info); - const makeUrl = conn.makeUrl.bind(conn); - - it('includes project ID and database ID', () => { - const url = makeUrl('Commit'); - expect(url).to.equal( - 'http://example.com/v1/projects/testproject/' + - 'databases/(default)/documents:commit' - ); - }); - }); - it('receives error messages', done => { const projectId = DEFAULT_PROJECT_ID; const info = getDefaultDatabaseInfo(); diff --git a/packages/firestore/test/integration/util/events_accumulator.ts b/packages/firestore/test/integration/util/events_accumulator.ts index f68b43cd91b..5bdb62220dc 100644 --- a/packages/firestore/test/integration/util/events_accumulator.ts +++ b/packages/firestore/test/integration/util/events_accumulator.ts @@ -16,7 +16,7 @@ */ import * as firestore from '@firebase/firestore-types'; -import { Deferred } from '@firebase/util'; +import { Deferred } from '../../util/promise'; import { expect } from 'chai'; /** diff --git a/packages/firestore/test/unit/remote/rest_connection.test.ts b/packages/firestore/test/unit/remote/rest_connection.test.ts new file mode 100644 index 00000000000..a0153a1b0a5 --- /dev/null +++ b/packages/firestore/test/unit/remote/rest_connection.test.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 { expect } from 'chai'; +import { Stream } from '../../../src/remote/connection'; +import { DatabaseId, DatabaseInfo } from '../../../src/core/database_info'; +import { RestConnection } from '../../../src/remote/rest_connection'; +import { Token } from '../../../src/api/credentials'; +import { StringMap } from '../../../src/util/types'; +import { Code, FirestoreError } from '../../../src/util/error'; +import { User } from '../../../src/auth/user'; +import { SDK_VERSION } from '../../../src/core/version'; +import { Indexable } from '../../../src/util/misc'; + +export class TestRestConnection extends RestConnection { + lastUrl: string = ''; + lastHeaders: StringMap = {}; + lastRequestBody: unknown = {}; + nextResponse = Promise.resolve({}); + + openStream( + rpcName: string, + token: Token | null + ): Stream { + throw new Error('Not Implemented'); + } + + protected performRPCRequest( + rpcName: string, + url: string, + headers: StringMap, + body: Req + ): Promise { + this.lastUrl = url; + this.lastRequestBody = (body as unknown) as Indexable; + this.lastHeaders = headers; + const response = this.nextResponse; + this.nextResponse = Promise.resolve({}); + return response as Promise; + } +} + +describe('RestConnection', () => { + const testDatabaseInfo = new DatabaseInfo( + new DatabaseId('testproject'), + 'persistenceKey', + 'example.com', + /*ssl=*/ false, + /*forceLongPolling=*/ false + ); + const connection = new TestRestConnection(testDatabaseInfo); + + it('url uses from path', async () => { + await connection.invokeRPC( + 'Commit', + 'projects/testproject/databases/(default)/documents', + {}, + null + ); + expect(connection.lastUrl).to.equal( + 'http://example.com/v1/projects/testproject/databases/(default)/documents:commit' + ); + }); + + it('merges headers', async () => { + await connection.invokeRPC( + 'RunQuery', + 'projects/testproject/databases/(default)/documents/foo', + {}, + { + user: User.UNAUTHENTICATED, + type: 'OAuth', + authHeaders: { 'Authorization': 'Bearer owner' } + } + ); + expect(connection.lastHeaders).to.deep.equal({ + 'Authorization': 'Bearer owner', + 'Content-Type': 'text/plain', + 'X-Goog-Api-Client': `gl-js/ fire/${SDK_VERSION}` + }); + }); + + it('returns success', async () => { + connection.nextResponse = Promise.resolve({ response: true }); + const response = await connection.invokeRPC( + 'RunQuery', + 'projects/testproject/databases/(default)/documents/coll', + {}, + null + ); + expect(response).to.deep.equal({ response: true }); + }); + + it('returns error', () => { + const error = new FirestoreError(Code.UNKNOWN, 'Test exception'); + connection.nextResponse = Promise.reject(error); + return expect( + connection.invokeRPC( + 'RunQuery', + 'projects/testproject/databases/(default)/documents/coll', + {}, + null + ) + ).to.be.eventually.rejectedWith(error); + }); +});