Skip to content
5 changes: 5 additions & 0 deletions .changeset/funny-weeks-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@firebase/data-connect": patch
---

Fixed issue where multiple calls to connectDataConnectEmulator caused an exception
6 changes: 6 additions & 0 deletions common/api-review/data-connect.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { FirebaseError } from '@firebase/util';
import { LogLevelString } from '@firebase/logger';
import { Provider } from '@firebase/component';

// @public (undocumented)
export function areTransportOptionsEqual(transportOptions1: TransportOptions, transportOptions2: TransportOptions): boolean;

// @public (undocumented)
export interface CancellableOperation<T> extends PromiseLike<{
data: T;
Expand Down Expand Up @@ -159,6 +162,9 @@ export function queryRef<Data>(dcInstance: DataConnect, queryName: string): Quer
// @public
export function queryRef<Data, Variables>(dcInstance: DataConnect, queryName: string, variables: Variables): QueryRef<Data, Variables>;

// @public (undocumented)
export function queryRef<Data, Variables>(dcInstance: DataConnect, serializedRef: SerializedRef<Data, Variables>): QueryRef<Data, Variables>;

// @public
export interface QueryResult<Data, Variables> extends DataConnectResult<Data, Variables> {
// (undocumented)
Expand Down
15 changes: 14 additions & 1 deletion packages/data-connect/src/api/DataConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,10 @@ export class DataConnect {

// @internal
enableEmulator(transportOptions: TransportOptions): void {
if (this._initialized) {
if (
this._initialized &&
!areTransportOptionsEqual(this._transportOptions, transportOptions)
) {
logError('enableEmulator called after initialization');
throw new DataConnectError(
Code.ALREADY_INITIALIZED,
Expand All @@ -191,6 +194,16 @@ export class DataConnect {
}
}

// @internal
export function areTransportOptionsEqual(
transportOptions1: TransportOptions,
transportOptions2: TransportOptions
) {
return (
JSON.stringify(transportOptions1) === JSON.stringify(transportOptions2)
);
}

/**
* Connect to the DataConnect Emulator
* @param dc Data Connect instance
Expand Down
36 changes: 31 additions & 5 deletions packages/data-connect/src/api/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,27 +104,53 @@ export function queryRef<Data, Variables>(
queryName: string,
variables: Variables
): QueryRef<Data, Variables>;

export function queryRef<Data, Variables>(
dcInstance: DataConnect,
serializedRef: SerializedRef<Data, Variables>
): QueryRef<Data, Variables>;

/**
* Execute Query
* @param dcInstance Data Connect instance to use.
* @param queryName Query to execute
* @param queryNameOrSerializedRef Query to execute
* @param variables Variables to execute with
* @param initialCache initial cache to use for client hydration
* @returns `QueryRef`
*/
export function queryRef<Data, Variables>(
dcInstance: DataConnect,
queryName: string,
queryNameOrSerializedRef: string | SerializedRef<Data, Variables>,
variables?: Variables,
initialCache?: QueryResult<Data, Variables>
): QueryRef<Data, Variables> {
dcInstance.setInitialized();
dcInstance._queryManager.track(queryName, variables, initialCache);
if (typeof queryNameOrSerializedRef === 'string') {
dcInstance._queryManager.track(
queryNameOrSerializedRef,
variables,
initialCache
);
} else {
dcInstance._queryManager.track(
queryNameOrSerializedRef.refInfo.name,
queryNameOrSerializedRef.refInfo.variables,
queryNameOrSerializedRef
);
}
const vars =
typeof queryNameOrSerializedRef !== 'string'
? queryNameOrSerializedRef.refInfo.variables
: variables;
const name =
typeof queryNameOrSerializedRef !== 'string'
? queryNameOrSerializedRef.refInfo.name
: queryNameOrSerializedRef;
return {
dataConnect: dcInstance,
refType: QUERY_STR,
name: queryName,
variables: variables as Variables
name,
variables: vars
};
}
/**
Expand Down
13 changes: 12 additions & 1 deletion packages/data-connect/test/queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ import {
subscribe,
terminate,
SOURCE_CACHE,
SOURCE_SERVER
SOURCE_SERVER,
toQueryRef
} from '../src';

import { setupQueries } from './emulatorSeeder';
import { getConnectionConfig, initDatabase, PROJECT_ID } from './util';
import { serialize } from 'v8';

use(chaiAsPromised);

Expand Down Expand Up @@ -136,6 +138,15 @@ describe('DataConnect Tests', async () => {
expect(result.data).to.eq(queryResult.data);
expect(result.source).to.eq(SOURCE_CACHE);
});

it(`returns the result source as cache when data already exists`, async () => {
const taskListQuery = queryRef<TaskListResponse>(dc, 'listPosts');
const queryResult = await executeQuery(taskListQuery);
const serializedRef = queryResult.toJSON();
const newRef = queryRef(dc, serializedRef);
expect(newRef.name).to.eq(serializedRef.refInfo.name);
expect(newRef.variables).to.eq(serializedRef.refInfo.variables);
});
it(`returns the proper JSON when calling .toJSON()`, async () => {
const taskListQuery = queryRef<TaskListResponse>(dc, 'listPosts');
await executeQuery(taskListQuery);
Expand Down
80 changes: 80 additions & 0 deletions packages/data-connect/test/unit/transportoptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @license
* Copyright 2024 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 {
TransportOptions,
areTransportOptionsEqual,
connectDataConnectEmulator,
getDataConnect
} from '../../src/api/DataConnect';
import { app } from '../util';
import { queryRef } from '../../src';
describe.only('Transport Options', () => {
it('should return false if transport options are not equal', () => {
const transportOptions1: TransportOptions = {
host: 'h',
port: 1,
sslEnabled: false
};
const transportOptions2: TransportOptions = {
host: 'h2',
port: 2,
sslEnabled: false
};
expect(
areTransportOptionsEqual(transportOptions1, transportOptions2)
).to.eq(false);
});
it('should return true if transport options are equal', () => {
const transportOptions1: TransportOptions = {
host: 'h',
port: 1,
sslEnabled: false
};
const transportOptions2: TransportOptions = {
host: 'h',
port: 1,
sslEnabled: false
};
expect(
areTransportOptionsEqual(transportOptions1, transportOptions2)
).to.eq(true);
});
it.only('should throw if emulator is connected to with new transport options', () => {
const dc = getDataConnect(app, {
connector: 'c',
location: 'l',
service: 's'
});
expect(() => connectDataConnectEmulator(dc, 'h', 80, false)).to.not.throw();
queryRef(dc, 'query');
expect(() => connectDataConnectEmulator(dc, 'h2', 80, false)).to.throw(
'DataConnect instance already initialized!'
);
});
it.only('should not throw if emulator is connected to with the same transport options', () => {
const dc = getDataConnect(app, {
connector: 'c',
location: 'l',
service: 's'
});
expect(() => connectDataConnectEmulator(dc, 'h', 80, false)).to.not.throw();
queryRef(dc, 'query');
expect(() => connectDataConnectEmulator(dc, 'h', 80, false)).to.not.throw();
});
});
Loading