From 2aa073884f3061f00b73d369f1a586ce73903e9d Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Mon, 2 Mar 2026 14:43:32 -0800 Subject: [PATCH 1/8] update validateArgs to handle new options arguments --- .../src/core/query/queryOptions.ts | 4 +- .../data-connect/src/util/validateArgs.ts | 81 ++++++++++++++----- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/packages/data-connect/src/core/query/queryOptions.ts b/packages/data-connect/src/core/query/queryOptions.ts index 6afb1fe94d..d7cec22e44 100644 --- a/packages/data-connect/src/core/query/queryOptions.ts +++ b/packages/data-connect/src/core/query/queryOptions.ts @@ -23,11 +23,13 @@ export const QueryFetchPolicy = { /* * Represents policy for how executeQuery fetches data - * */ export type QueryFetchPolicy = (typeof QueryFetchPolicy)[keyof typeof QueryFetchPolicy]; +/** + * Options for executing a query. + */ export interface ExecuteQueryOptions { fetchPolicy: QueryFetchPolicy; } diff --git a/packages/data-connect/src/util/validateArgs.ts b/packages/data-connect/src/util/validateArgs.ts index e957786aa4..bb9ebee1d5 100644 --- a/packages/data-connect/src/util/validateArgs.ts +++ b/packages/data-connect/src/util/validateArgs.ts @@ -15,44 +15,89 @@ * limitations under the License. */ +import { ExecuteQueryOptions } from '../api'; import { ConnectorConfig, DataConnect, getDataConnect } from '../api/DataConnect'; import { Code, DataConnectError } from '../core/error'; + interface ParsedArgs { dc: DataConnect; vars: Variables; + options?: ExecuteQueryOptions; } /** - * The generated SDK will allow the user to pass in either the variable or the data connect instance with the variable, - * and this function validates the variables and returns back the DataConnect instance and variables based on the arguments passed in. + * The generated SDK will allow the user to pass in either the variables or the data connect instance + * with the variables, and/or options. This function validates the variables and returns back the + * DataConnect instance and variables, and potentially options, based on the arguments passed in. + * + * Generated SDKs generated from versions 3.2.0 and lower of the Data Connect emulator binary are not + * concerned with any options, and they will never provide the {hasVars} parameter. + * * @param connectorConfig - * @param dcOrVars - * @param vars - * @param validateVars - * @returns {DataConnect} and {Variables} instance + * @param dcOrVarsOrOptions + * @param varsOrOptions + * @param variablesRequired + * @param hasVars - only provided by generated SDKs using Data Connect emulator binary version higher than 3.2.0 + * @param options - only provided by generated SDKs using Data Connect emulator binary version higher than 3.2.0 + * @returns {DataConnect} and {Variables} instance, and optionally {ExecuteQueryOptions} * @internal */ export function validateArgs( connectorConfig: ConnectorConfig, - dcOrVars?: DataConnect | Variables, - vars?: Variables, - validateVars?: boolean + dcOrVarsOrOptions?: DataConnect | Variables, + varsOrOptions?: Variables, + variablesRequired?: boolean, + hasVars?: boolean, + options?: ExecuteQueryOptions ): ParsedArgs { let dcInstance: DataConnect; let realVars: Variables; - if (dcOrVars && 'enableEmulator' in dcOrVars) { - dcInstance = dcOrVars as DataConnect; - realVars = vars as Variables; + let realOptions: ExecuteQueryOptions; + + const legacyNoOptions = typeof hasVars === 'undefined'; + + const dcFirstArg = dcOrVarsOrOptions && 'enableEmulator' in dcOrVarsOrOptions; + + if (legacyNoOptions) { + // legacy gen SDKs are not concerned with options - just dc and variables + if (dcFirstArg) { + dcInstance = dcOrVarsOrOptions as DataConnect; + realVars = varsOrOptions as Variables; + } else { + dcInstance = getDataConnect(connectorConfig); + realVars = dcOrVarsOrOptions as Variables; + } + if (!dcInstance || (!realVars && variablesRequired)) { + throw new DataConnectError(Code.INVALID_ARGUMENT, 'Variables required.'); + } + return { dc: dcInstance, vars: realVars }; } else { - dcInstance = getDataConnect(connectorConfig); - realVars = dcOrVars as Variables; - } - if (!dcInstance || (!realVars && validateVars)) { - throw new DataConnectError(Code.INVALID_ARGUMENT, 'Variables required.'); + if (dcFirstArg) { + dcInstance = dcOrVarsOrOptions as DataConnect; + if (hasVars) { + realVars = varsOrOptions as Variables; + realOptions = options as ExecuteQueryOptions; + } else { + realVars = undefined as unknown as Variables; + realOptions = varsOrOptions as ExecuteQueryOptions; + } + } else { + dcInstance = getDataConnect(connectorConfig); + if (hasVars) { + realVars = dcOrVarsOrOptions as Variables; + realOptions = varsOrOptions as ExecuteQueryOptions; + } else { + realVars = undefined as unknown as Variables; + realOptions = dcOrVarsOrOptions as ExecuteQueryOptions; + } + } + if (!dcInstance || (!realVars && variablesRequired)) { + throw new DataConnectError(Code.INVALID_ARGUMENT, 'Variables required.'); + } + return { dc: dcInstance, vars: realVars, options: realOptions }; } - return { dc: dcInstance, vars: realVars }; } From f176ef88a2f9c663aea493ba887b5e374247a808 Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Mon, 2 Mar 2026 15:51:38 -0800 Subject: [PATCH 2/8] add validateArgs unit tests --- common/api-review/data-connect.api.md | 2 +- .../data-connect/src/util/validateArgs.ts | 10 +- .../test/unit/validateArgs.test.ts | 299 ++++++++++++++++++ 3 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 packages/data-connect/test/unit/validateArgs.test.ts diff --git a/common/api-review/data-connect.api.md b/common/api-review/data-connect.api.md index c8f4814764..c1994ade96 100644 --- a/common/api-review/data-connect.api.md +++ b/common/api-review/data-connect.api.md @@ -165,7 +165,7 @@ export function executeMutation(mutationRef: MutationRef(queryRef: QueryRef, options?: ExecuteQueryOptions): QueryPromise; -// @public (undocumented) +// @public export interface ExecuteQueryOptions { // (undocumented) fetchPolicy: QueryFetchPolicy; diff --git a/packages/data-connect/src/util/validateArgs.ts b/packages/data-connect/src/util/validateArgs.ts index bb9ebee1d5..6c897f316a 100644 --- a/packages/data-connect/src/util/validateArgs.ts +++ b/packages/data-connect/src/util/validateArgs.ts @@ -30,11 +30,11 @@ interface ParsedArgs { } /** - * The generated SDK will allow the user to pass in either the variables or the data connect instance - * with the variables, and/or options. This function validates the variables and returns back the + * The generated SDK will allow the user to pass in either the variables or the data connect instance + * with the variables, and/or options. This function validates the variables and returns back the * DataConnect instance and variables, and potentially options, based on the arguments passed in. * - * Generated SDKs generated from versions 3.2.0 and lower of the Data Connect emulator binary are not + * Generated SDKs generated from versions 3.2.0 and lower of the Data Connect emulator binary are not * concerned with any options, and they will never provide the {hasVars} parameter. * * @param connectorConfig @@ -48,8 +48,8 @@ interface ParsedArgs { */ export function validateArgs( connectorConfig: ConnectorConfig, - dcOrVarsOrOptions?: DataConnect | Variables, - varsOrOptions?: Variables, + dcOrVarsOrOptions?: DataConnect | Variables | ExecuteQueryOptions, + varsOrOptions?: Variables | ExecuteQueryOptions, variablesRequired?: boolean, hasVars?: boolean, options?: ExecuteQueryOptions diff --git a/packages/data-connect/test/unit/validateArgs.test.ts b/packages/data-connect/test/unit/validateArgs.test.ts new file mode 100644 index 0000000000..32fbd52a4a --- /dev/null +++ b/packages/data-connect/test/unit/validateArgs.test.ts @@ -0,0 +1,299 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import * as dataConnectIndex from '../../src/api/DataConnect'; +import { DataConnect, ConnectorConfig } from '../../src/api/DataConnect'; +import { Code, DataConnectError } from '../../src/core/error'; +import { QueryFetchPolicy } from '../../src/core/query/queryOptions'; +import { validateArgs } from '../../src/util/validateArgs'; + +interface IdVars { + id: string; +} + +describe('validateArgs()', () => { + let getDataConnectStub: sinon.SinonStub; + + const connectorConfig: ConnectorConfig = { + location: 'us-west2', + service: 'my-service', + connector: 'my-connector' + }; + + const providedDcInstance = { + app: { options: { projectId: 'my-project' } }, + dataConnectOptions: connectorConfig, + source: 'PROVIDED', + enableEmulator: sinon.stub() + } as unknown as DataConnect; + + const variables: IdVars = { id: 'stephenarosaj' }; + const options = { fetchPolicy: QueryFetchPolicy.SERVER_ONLY }; + + const stubDcInstance = { + app: { options: { projectId: 'my-project' } }, + dataConnectOptions: connectorConfig, + source: 'STUB' + } as unknown as DataConnect; + + beforeEach(() => { + getDataConnectStub = sinon + .stub(dataConnectIndex, 'getDataConnect') + .returns(stubDcInstance); + }); + + afterEach(() => { + getDataConnectStub.restore(); + }); + + describe('legacy mode (hasVars undefined)', () => { + describe('should parse arguments properly', () => { + it('when dc and vars are provided', () => { + const { dc: dcInstance, vars: inputVars } = validateArgs( + connectorConfig, + providedDcInstance, + variables + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.deep.equal(variables); + }); + + it('when only vars are provided (infer dc)', () => { + const { dc: dcInstance, vars: inputVars } = validateArgs( + connectorConfig, + variables + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.deep.equal(variables); + }); + + it('when no args are provided (infer dc)', () => { + const { dc: dcInstance, vars: inputVars } = + validateArgs(connectorConfig); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.be.undefined; + }); + }); + + describe('should throw when vars are required but missing', () => { + it('and dc is provided', () => { + expect(() => { + validateArgs( + connectorConfig, + providedDcInstance, + undefined, + /** variablesRequired = */ true + ); + }) + .to.throw(DataConnectError, 'Variables required') + .with.property('code', Code.INVALID_ARGUMENT); + }); + it('and dc is NOT provided', () => { + expect(() => { + validateArgs( + connectorConfig, + undefined, + undefined, + /** variablesRequired = */ true + ); + }) + .to.throw(DataConnectError, 'Variables required') + .with.property('code', Code.INVALID_ARGUMENT); + }); + }); + + describe('should not throw when vars are optional and missing', () => { + it('and dc is provided', () => { + const { dc: dcInstance, vars: inputVars } = validateArgs( + connectorConfig, + providedDcInstance, + undefined, + /** variablesRequired = */ false + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.be.undefined; + }); + it('and dc is NOT provided', () => { + const { dc: dcInstance, vars: inputVars } = validateArgs( + connectorConfig, + undefined, + undefined, + /** variablesRequired = */ false + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.be.undefined; + }); + }); + }); + + describe('modern mode (version > 3.2.0, hasVars defined)', () => { + describe('should parse arguments properly', () => { + it('when dc, vars, and options are provided', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgs( + connectorConfig, + providedDcInstance, + variables, + /** variablesRequired = */ true, + /** hasVars = */ true, + options + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.deep.equal(variables); + expect(inputOpts).to.deep.equal(options); + }); + + it('when vars and options are provided (infer dc)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgs( + connectorConfig, + variables, + options, + /** variablesRequired = */ true, + /** hasVars = */ true + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.deep.equal(variables); + expect(inputOpts).to.deep.equal(options); + }); + + it('when dc and vars are provided (no options)', () => { + const { dc: dcInstance, vars: inputVars } = validateArgs( + connectorConfig, + providedDcInstance, + variables, + /** variablesRequired = */ true, + /** hasVars = */ true + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.deep.equal(variables); + }); + + it('when only vars are provided (no options, infer dc)', () => { + const { dc: dcInstance, vars: inputVars } = validateArgs( + connectorConfig, + variables, + undefined, + /** variablesRequired = */ true, + /** hasVars = */ true + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.deep.equal(variables); + }); + + it('when dc and options are provided (no vars)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgs( + connectorConfig, + providedDcInstance, + options, + /** variablesRequired = */ false, + /** hasVars = */ false + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.be.undefined; + expect(inputOpts).to.deep.equal(options); + }); + + it('when only options are provided (no vars, infer dc)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgs( + connectorConfig, + options, + undefined, + /** variablesRequired = */ false, + /** hasVars = */ false + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.be.undefined; + expect(inputOpts).to.deep.equal(options); + }); + + it('when no args are provided (infer dc)', () => { + const { dc: dcInstance, vars: inputVars } = validateArgs( + connectorConfig, + undefined, + undefined, + /** variablesRequired = */ false, + /** hasVars = */ true + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.be.undefined; + }); + }); + + describe('should throw when vars are required but missing', () => { + it('and dc is provided', () => { + expect(() => { + validateArgs( + connectorConfig, + providedDcInstance, + undefined, + /** variablesRequired = */ true, + /** hasVars = */ true + ); + }) + .to.throw(DataConnectError, 'Variables required') + .with.property('code', Code.INVALID_ARGUMENT); + }); + it('and dc is NOT provided', () => { + expect(() => { + validateArgs( + connectorConfig, + undefined, + undefined, + /** variablesRequired = */ true, + /** hasVars = */ true + ); + }) + .to.throw(DataConnectError, 'Variables required') + .with.property('code', Code.INVALID_ARGUMENT); + }); + }); + + describe('should not throw when vars are optional and missing', () => { + it('and dc is provided', () => { + const { dc: dcInstance, vars: inputVars } = validateArgs( + connectorConfig, + providedDcInstance, + undefined, + /** variablesRequired = */ false, + /** hasVars = */ true + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.be.undefined; + }); + it('and dc is NOT provided', () => { + const { dc: dcInstance, vars: inputVars } = validateArgs( + connectorConfig, + undefined, + undefined, + /** variablesRequired = */ false, + /** hasVars = */ true + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.be.undefined; + }); + }); + }); +}); From 2d042696d29d55066dde99173d09a1c6a00f6ce8 Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Mon, 2 Mar 2026 16:35:02 -0800 Subject: [PATCH 3/8] split functionality into separate functions, add changeset --- .changeset/four-bottles-collect.md | 5 + .../data-connect/src/util/validateArgs.ts | 106 ++++++----- .../test/unit/validateArgs.test.ts | 178 ++++++++---------- 3 files changed, 145 insertions(+), 144 deletions(-) create mode 100644 .changeset/four-bottles-collect.md diff --git a/.changeset/four-bottles-collect.md b/.changeset/four-bottles-collect.md new file mode 100644 index 0000000000..a62333fcda --- /dev/null +++ b/.changeset/four-bottles-collect.md @@ -0,0 +1,5 @@ +--- +'@firebase/data-connect': minor +--- + +Add validateArgsWithOptions() diff --git a/packages/data-connect/src/util/validateArgs.ts b/packages/data-connect/src/util/validateArgs.ts index 6c897f316a..9215953bd6 100644 --- a/packages/data-connect/src/util/validateArgs.ts +++ b/packages/data-connect/src/util/validateArgs.ts @@ -29,75 +29,89 @@ interface ParsedArgs { options?: ExecuteQueryOptions; } +/** + * The generated SDK will allow the user to pass in either the variables or the data connect instance + * with the variables. This function validates the variables and returns back the DataConnect instance + * and variables based on the arguments passed in. + * + * Generated SDKs generated from versions 3.2.0 and lower of the Data Connect emulator binary are + * NOT concerned with options, and will use this function to validate arguments. + * + * @param connectorConfig + * @param dcOrVars + * @param vars + * @param variablesRequired + * @returns {DataConnect} and {Variables} instance + * @internal + */ +export function validateArgs( + connectorConfig: ConnectorConfig, + dcOrVars?: DataConnect | Variables, + vars?: Variables, + variablesRequired?: boolean +): ParsedArgs { + let dcInstance: DataConnect; + let realVars: Variables; + + const dcFirstArg = dcOrVars && 'enableEmulator' in dcOrVars; + + if (dcFirstArg) { + dcInstance = dcOrVars as DataConnect; + realVars = vars as Variables; + } else { + dcInstance = getDataConnect(connectorConfig); + realVars = dcOrVars as Variables; + } + + if (!dcInstance || (!realVars && variablesRequired)) { + throw new DataConnectError(Code.INVALID_ARGUMENT, 'Variables required.'); + } + + return { dc: dcInstance, vars: realVars }; +} + /** * The generated SDK will allow the user to pass in either the variables or the data connect instance * with the variables, and/or options. This function validates the variables and returns back the * DataConnect instance and variables, and potentially options, based on the arguments passed in. * - * Generated SDKs generated from versions 3.2.0 and lower of the Data Connect emulator binary are not - * concerned with any options, and they will never provide the {hasVars} parameter. + * Generated SDKs generated from versions 3.2.0 and higher of the Data Connect emulator binary are + * in fact concerned with options, and will use this function to validate arguments. * * @param connectorConfig * @param dcOrVarsOrOptions * @param varsOrOptions * @param variablesRequired - * @param hasVars - only provided by generated SDKs using Data Connect emulator binary version higher than 3.2.0 - * @param options - only provided by generated SDKs using Data Connect emulator binary version higher than 3.2.0 + * @param options * @returns {DataConnect} and {Variables} instance, and optionally {ExecuteQueryOptions} * @internal */ -export function validateArgs( +export function validateArgsWithOptions( connectorConfig: ConnectorConfig, dcOrVarsOrOptions?: DataConnect | Variables | ExecuteQueryOptions, varsOrOptions?: Variables | ExecuteQueryOptions, - variablesRequired?: boolean, - hasVars?: boolean, - options?: ExecuteQueryOptions + options?: ExecuteQueryOptions, + variablesRequired?: boolean ): ParsedArgs { let dcInstance: DataConnect; let realVars: Variables; let realOptions: ExecuteQueryOptions; - const legacyNoOptions = typeof hasVars === 'undefined'; - const dcFirstArg = dcOrVarsOrOptions && 'enableEmulator' in dcOrVarsOrOptions; - if (legacyNoOptions) { - // legacy gen SDKs are not concerned with options - just dc and variables - if (dcFirstArg) { - dcInstance = dcOrVarsOrOptions as DataConnect; - realVars = varsOrOptions as Variables; - } else { - dcInstance = getDataConnect(connectorConfig); - realVars = dcOrVarsOrOptions as Variables; - } - if (!dcInstance || (!realVars && variablesRequired)) { - throw new DataConnectError(Code.INVALID_ARGUMENT, 'Variables required.'); - } - return { dc: dcInstance, vars: realVars }; + if (dcFirstArg) { + dcInstance = dcOrVarsOrOptions as DataConnect; + realVars = varsOrOptions as Variables; + realOptions = options as ExecuteQueryOptions; } else { - if (dcFirstArg) { - dcInstance = dcOrVarsOrOptions as DataConnect; - if (hasVars) { - realVars = varsOrOptions as Variables; - realOptions = options as ExecuteQueryOptions; - } else { - realVars = undefined as unknown as Variables; - realOptions = varsOrOptions as ExecuteQueryOptions; - } - } else { - dcInstance = getDataConnect(connectorConfig); - if (hasVars) { - realVars = dcOrVarsOrOptions as Variables; - realOptions = varsOrOptions as ExecuteQueryOptions; - } else { - realVars = undefined as unknown as Variables; - realOptions = dcOrVarsOrOptions as ExecuteQueryOptions; - } - } - if (!dcInstance || (!realVars && variablesRequired)) { - throw new DataConnectError(Code.INVALID_ARGUMENT, 'Variables required.'); - } - return { dc: dcInstance, vars: realVars, options: realOptions }; + dcInstance = getDataConnect(connectorConfig); + realVars = dcOrVarsOrOptions as Variables; + realOptions = varsOrOptions as ExecuteQueryOptions; } + + if (!dcInstance || (!realVars && variablesRequired)) { + throw new DataConnectError(Code.INVALID_ARGUMENT, 'Variables required.'); + } + + return { dc: dcInstance, vars: realVars, options: realOptions }; } diff --git a/packages/data-connect/test/unit/validateArgs.test.ts b/packages/data-connect/test/unit/validateArgs.test.ts index 32fbd52a4a..ee397f6224 100644 --- a/packages/data-connect/test/unit/validateArgs.test.ts +++ b/packages/data-connect/test/unit/validateArgs.test.ts @@ -1,3 +1,20 @@ +/** + * @license + * Copyright 2026 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 * as sinon from 'sinon'; @@ -5,7 +22,10 @@ import * as dataConnectIndex from '../../src/api/DataConnect'; import { DataConnect, ConnectorConfig } from '../../src/api/DataConnect'; import { Code, DataConnectError } from '../../src/core/error'; import { QueryFetchPolicy } from '../../src/core/query/queryOptions'; -import { validateArgs } from '../../src/util/validateArgs'; +import { + validateArgs, + validateArgsWithOptions +} from '../../src/util/validateArgs'; interface IdVars { id: string; @@ -46,7 +66,7 @@ describe('validateArgs()', () => { getDataConnectStub.restore(); }); - describe('legacy mode (hasVars undefined)', () => { + describe('validateArgs', () => { describe('should parse arguments properly', () => { it('when dc and vars are provided', () => { const { dc: dcInstance, vars: inputVars } = validateArgs( @@ -103,45 +123,19 @@ describe('validateArgs()', () => { .with.property('code', Code.INVALID_ARGUMENT); }); }); - - describe('should not throw when vars are optional and missing', () => { - it('and dc is provided', () => { - const { dc: dcInstance, vars: inputVars } = validateArgs( - connectorConfig, - providedDcInstance, - undefined, - /** variablesRequired = */ false - ); - expect(dcInstance).to.deep.equal(providedDcInstance); - expect(inputVars).to.be.undefined; - }); - it('and dc is NOT provided', () => { - const { dc: dcInstance, vars: inputVars } = validateArgs( - connectorConfig, - undefined, - undefined, - /** variablesRequired = */ false - ); - expect(getDataConnectStub.calledOnce).to.be.true; - expect(dcInstance).to.deep.equal(stubDcInstance); - expect(inputVars).to.be.undefined; - }); - }); }); - describe('modern mode (version > 3.2.0, hasVars defined)', () => { + describe('validateArgsWithOptions', () => { describe('should parse arguments properly', () => { it('when dc, vars, and options are provided', () => { const { dc: dcInstance, vars: inputVars, options: inputOpts - } = validateArgs( + } = validateArgsWithOptions( connectorConfig, providedDcInstance, variables, - /** variablesRequired = */ true, - /** hasVars = */ true, options ); expect(dcInstance).to.deep.equal(providedDcInstance); @@ -154,13 +148,7 @@ describe('validateArgs()', () => { dc: dcInstance, vars: inputVars, options: inputOpts - } = validateArgs( - connectorConfig, - variables, - options, - /** variablesRequired = */ true, - /** hasVars = */ true - ); + } = validateArgsWithOptions(connectorConfig, variables, options); expect(getDataConnectStub.calledOnce).to.be.true; expect(dcInstance).to.deep.equal(stubDcInstance); expect(inputVars).to.deep.equal(variables); @@ -168,28 +156,30 @@ describe('validateArgs()', () => { }); it('when dc and vars are provided (no options)', () => { - const { dc: dcInstance, vars: inputVars } = validateArgs( + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( connectorConfig, providedDcInstance, - variables, - /** variablesRequired = */ true, - /** hasVars = */ true + variables ); expect(dcInstance).to.deep.equal(providedDcInstance); expect(inputVars).to.deep.equal(variables); + expect(inputOpts).to.be.undefined; }); it('when only vars are provided (no options, infer dc)', () => { - const { dc: dcInstance, vars: inputVars } = validateArgs( - connectorConfig, - variables, - undefined, - /** variablesRequired = */ true, - /** hasVars = */ true - ); + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions(connectorConfig, variables); expect(getDataConnectStub.calledOnce).to.be.true; expect(dcInstance).to.deep.equal(stubDcInstance); expect(inputVars).to.deep.equal(variables); + expect(inputOpts).to.be.undefined; }); it('when dc and options are provided (no vars)', () => { @@ -197,12 +187,11 @@ describe('validateArgs()', () => { dc: dcInstance, vars: inputVars, options: inputOpts - } = validateArgs( + } = validateArgsWithOptions( connectorConfig, providedDcInstance, - options, - /** variablesRequired = */ false, - /** hasVars = */ false + undefined, + options ); expect(dcInstance).to.deep.equal(providedDcInstance); expect(inputVars).to.be.undefined; @@ -214,13 +203,7 @@ describe('validateArgs()', () => { dc: dcInstance, vars: inputVars, options: inputOpts - } = validateArgs( - connectorConfig, - options, - undefined, - /** variablesRequired = */ false, - /** hasVars = */ false - ); + } = validateArgsWithOptions(connectorConfig, undefined, options); expect(getDataConnectStub.calledOnce).to.be.true; expect(dcInstance).to.deep.equal(stubDcInstance); expect(inputVars).to.be.undefined; @@ -228,71 +211,70 @@ describe('validateArgs()', () => { }); it('when no args are provided (infer dc)', () => { - const { dc: dcInstance, vars: inputVars } = validateArgs( - connectorConfig, - undefined, - undefined, - /** variablesRequired = */ false, - /** hasVars = */ true - ); + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions(connectorConfig); expect(getDataConnectStub.calledOnce).to.be.true; expect(dcInstance).to.deep.equal(stubDcInstance); expect(inputVars).to.be.undefined; + expect(inputOpts).to.be.undefined; }); }); describe('should throw when vars are required but missing', () => { - it('and dc is provided', () => { + it('and only dc is provided', () => { expect(() => { - validateArgs( + validateArgsWithOptions( connectorConfig, providedDcInstance, undefined, - /** variablesRequired = */ true, - /** hasVars = */ true + undefined, + /** variablesRequired = */ true ); }) .to.throw(DataConnectError, 'Variables required') .with.property('code', Code.INVALID_ARGUMENT); }); - it('and dc is NOT provided', () => { + it('and only options is provided', () => { expect(() => { - validateArgs( + validateArgsWithOptions( connectorConfig, undefined, + options, undefined, - /** variablesRequired = */ true, - /** hasVars = */ true + /** variablesRequired = */ true ); }) .to.throw(DataConnectError, 'Variables required') .with.property('code', Code.INVALID_ARGUMENT); }); - }); - - describe('should not throw when vars are optional and missing', () => { - it('and dc is provided', () => { - const { dc: dcInstance, vars: inputVars } = validateArgs( - connectorConfig, - providedDcInstance, - undefined, - /** variablesRequired = */ false, - /** hasVars = */ true - ); - expect(dcInstance).to.deep.equal(providedDcInstance); - expect(inputVars).to.be.undefined; + it('and dc and options is provided', () => { + expect(() => { + validateArgsWithOptions( + connectorConfig, + providedDcInstance, + undefined, + options, + /** variablesRequired = */ true + ); + }) + .to.throw(DataConnectError, 'Variables required') + .with.property('code', Code.INVALID_ARGUMENT); }); - it('and dc is NOT provided', () => { - const { dc: dcInstance, vars: inputVars } = validateArgs( - connectorConfig, - undefined, - undefined, - /** variablesRequired = */ false, - /** hasVars = */ true - ); - expect(getDataConnectStub.calledOnce).to.be.true; - expect(dcInstance).to.deep.equal(stubDcInstance); - expect(inputVars).to.be.undefined; + it('and nothing is provided', () => { + expect(() => { + validateArgsWithOptions( + connectorConfig, + undefined, + undefined, + undefined, + /** variablesRequired = */ true + ); + }) + .to.throw(DataConnectError, 'Variables required') + .with.property('code', Code.INVALID_ARGUMENT); }); }); }); From 26ca0ef2d29236d719f59f3cde5d291c18394820 Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Mon, 2 Mar 2026 17:11:13 -0800 Subject: [PATCH 4/8] add hasVars argument to help differentiate first argument --- .../data-connect/src/util/validateArgs.ts | 19 +- .../test/unit/validateArgs.test.ts | 291 ++++++++++++------ 2 files changed, 220 insertions(+), 90 deletions(-) diff --git a/packages/data-connect/src/util/validateArgs.ts b/packages/data-connect/src/util/validateArgs.ts index 9215953bd6..03304fc426 100644 --- a/packages/data-connect/src/util/validateArgs.ts +++ b/packages/data-connect/src/util/validateArgs.ts @@ -91,6 +91,7 @@ export function validateArgsWithOptions( dcOrVarsOrOptions?: DataConnect | Variables | ExecuteQueryOptions, varsOrOptions?: Variables | ExecuteQueryOptions, options?: ExecuteQueryOptions, + hasVars?: boolean, variablesRequired?: boolean ): ParsedArgs { let dcInstance: DataConnect; @@ -101,12 +102,22 @@ export function validateArgsWithOptions( if (dcFirstArg) { dcInstance = dcOrVarsOrOptions as DataConnect; - realVars = varsOrOptions as Variables; - realOptions = options as ExecuteQueryOptions; + if (hasVars) { + realVars = varsOrOptions as Variables; + realOptions = options as ExecuteQueryOptions; + } else { + realVars = undefined as unknown as Variables; + realOptions = varsOrOptions as ExecuteQueryOptions; + } } else { dcInstance = getDataConnect(connectorConfig); - realVars = dcOrVarsOrOptions as Variables; - realOptions = varsOrOptions as ExecuteQueryOptions; + if (hasVars) { + realVars = dcOrVarsOrOptions as Variables; + realOptions = varsOrOptions as ExecuteQueryOptions; + } else { + realVars = undefined as unknown as Variables; + realOptions = dcOrVarsOrOptions as ExecuteQueryOptions; + } } if (!dcInstance || (!realVars && variablesRequired)) { diff --git a/packages/data-connect/test/unit/validateArgs.test.ts b/packages/data-connect/test/unit/validateArgs.test.ts index ee397f6224..05b66f2431 100644 --- a/packages/data-connect/test/unit/validateArgs.test.ts +++ b/packages/data-connect/test/unit/validateArgs.test.ts @@ -127,99 +127,211 @@ describe('validateArgs()', () => { describe('validateArgsWithOptions', () => { describe('should parse arguments properly', () => { - it('when dc, vars, and options are provided', () => { - const { - dc: dcInstance, - vars: inputVars, - options: inputOpts - } = validateArgsWithOptions( - connectorConfig, - providedDcInstance, - variables, - options - ); - expect(dcInstance).to.deep.equal(providedDcInstance); - expect(inputVars).to.deep.equal(variables); - expect(inputOpts).to.deep.equal(options); - }); + describe('with hasVars = true', () => { + it('when dc, vars, and options are provided', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + providedDcInstance, + variables, + options, + /** hasVars = */ true, + /** variablesRequired = */ false + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.deep.equal(variables); + expect(inputOpts).to.deep.equal(options); + }); - it('when vars and options are provided (infer dc)', () => { - const { - dc: dcInstance, - vars: inputVars, - options: inputOpts - } = validateArgsWithOptions(connectorConfig, variables, options); - expect(getDataConnectStub.calledOnce).to.be.true; - expect(dcInstance).to.deep.equal(stubDcInstance); - expect(inputVars).to.deep.equal(variables); - expect(inputOpts).to.deep.equal(options); - }); + it('when vars and options are provided (infer dc)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + variables, + options, + undefined, + /** hasVars = */ true, + /** variablesRequired = */ false + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.deep.equal(variables); + expect(inputOpts).to.deep.equal(options); + }); - it('when dc and vars are provided (no options)', () => { - const { - dc: dcInstance, - vars: inputVars, - options: inputOpts - } = validateArgsWithOptions( - connectorConfig, - providedDcInstance, - variables - ); - expect(dcInstance).to.deep.equal(providedDcInstance); - expect(inputVars).to.deep.equal(variables); - expect(inputOpts).to.be.undefined; - }); + it('when dc and vars are provided (no options)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + providedDcInstance, + variables, + undefined, + /** hasVars = */ true, + /** variablesRequired = */ false + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.deep.equal(variables); + expect(inputOpts).to.be.undefined; + }); - it('when only vars are provided (no options, infer dc)', () => { - const { - dc: dcInstance, - vars: inputVars, - options: inputOpts - } = validateArgsWithOptions(connectorConfig, variables); - expect(getDataConnectStub.calledOnce).to.be.true; - expect(dcInstance).to.deep.equal(stubDcInstance); - expect(inputVars).to.deep.equal(variables); - expect(inputOpts).to.be.undefined; - }); + it('when only vars are provided (no options, infer dc)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + variables, + undefined, + undefined, + /** hasVars = */ true, + /** variablesRequired = */ false + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.deep.equal(variables); + expect(inputOpts).to.be.undefined; + }); - it('when dc and options are provided (no vars)', () => { - const { - dc: dcInstance, - vars: inputVars, - options: inputOpts - } = validateArgsWithOptions( - connectorConfig, - providedDcInstance, - undefined, - options - ); - expect(dcInstance).to.deep.equal(providedDcInstance); - expect(inputVars).to.be.undefined; - expect(inputOpts).to.deep.equal(options); - }); + it('when dc and options are provided (optional vars are undefined)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + providedDcInstance, + undefined, + options, + /** hasVars = */ true, + /** variablesRequired = */ false + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.be.undefined; + expect(inputOpts).to.deep.equal(options); + }); - it('when only options are provided (no vars, infer dc)', () => { - const { - dc: dcInstance, - vars: inputVars, - options: inputOpts - } = validateArgsWithOptions(connectorConfig, undefined, options); - expect(getDataConnectStub.calledOnce).to.be.true; - expect(dcInstance).to.deep.equal(stubDcInstance); - expect(inputVars).to.be.undefined; - expect(inputOpts).to.deep.equal(options); + it('when only options is provided (infer dc, optional vars are undefined)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + undefined, + options, + undefined, + /** hasVars = */ true, + /** variablesRequired = */ false + ); + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.be.undefined; + expect(inputOpts).to.deep.equal(options); + }); + + it('when no args are provided (infer dc, optional vars are undefined)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + undefined, + undefined, + undefined, + /** hasVars = */ true, + /** variablesRequired = */ false + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.be.undefined; + expect(inputOpts).to.be.undefined; + }); }); - it('when no args are provided (infer dc)', () => { - const { - dc: dcInstance, - vars: inputVars, - options: inputOpts - } = validateArgsWithOptions(connectorConfig); - expect(getDataConnectStub.calledOnce).to.be.true; - expect(dcInstance).to.deep.equal(stubDcInstance); - expect(inputVars).to.be.undefined; - expect(inputOpts).to.be.undefined; + describe('with hasVars = false', () => { + it('when dc and options are provided', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + providedDcInstance, + options, + undefined, + /** hasVars = */ false, + /** variablesRequired = */ false + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.deep.equal(undefined); + expect(inputOpts).to.deep.equal(options); + }); + + it('when only dc is provided (no options)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + providedDcInstance, + undefined, + undefined, + /** hasVars = */ false, + /** variablesRequired = */ false + ); + expect(dcInstance).to.deep.equal(providedDcInstance); + expect(inputVars).to.deep.equal(undefined); + expect(inputOpts).to.be.undefined; + }); + + it('when only options are provided (infer dc)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + options, + undefined, + undefined, + /** hasVars = */ false, + /** variablesRequired = */ false + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.deep.equal(undefined); + expect(inputOpts).to.deep.equal(options); + }); + + it('when no args are provided (infer dc)', () => { + const { + dc: dcInstance, + vars: inputVars, + options: inputOpts + } = validateArgsWithOptions( + connectorConfig, + undefined, + undefined, + undefined, + /** hasVars = */ false, + /** variablesRequired = */ false + ); + expect(getDataConnectStub.calledOnce).to.be.true; + expect(dcInstance).to.deep.equal(stubDcInstance); + expect(inputVars).to.be.undefined; + expect(inputOpts).to.be.undefined; + }); }); }); @@ -231,12 +343,14 @@ describe('validateArgs()', () => { providedDcInstance, undefined, undefined, + /** hasVars = */ true, /** variablesRequired = */ true ); }) .to.throw(DataConnectError, 'Variables required') .with.property('code', Code.INVALID_ARGUMENT); }); + it('and only options is provided', () => { expect(() => { validateArgsWithOptions( @@ -244,12 +358,14 @@ describe('validateArgs()', () => { undefined, options, undefined, + /** hasVars = */ true, /** variablesRequired = */ true ); }) .to.throw(DataConnectError, 'Variables required') .with.property('code', Code.INVALID_ARGUMENT); }); + it('and dc and options is provided', () => { expect(() => { validateArgsWithOptions( @@ -257,12 +373,14 @@ describe('validateArgs()', () => { providedDcInstance, undefined, options, + /** hasVars = */ true, /** variablesRequired = */ true ); }) .to.throw(DataConnectError, 'Variables required') .with.property('code', Code.INVALID_ARGUMENT); }); + it('and nothing is provided', () => { expect(() => { validateArgsWithOptions( @@ -270,6 +388,7 @@ describe('validateArgs()', () => { undefined, undefined, undefined, + /** hasVars = */ true, /** variablesRequired = */ true ); }) From d50fead1d2c23647c89377ee451bce199e45499f Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Mon, 2 Mar 2026 17:15:29 -0800 Subject: [PATCH 5/8] update changelog --- .changeset/four-bottles-collect.md | 5 ----- .changeset/funny-apples-pay.md | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) delete mode 100644 .changeset/four-bottles-collect.md create mode 100644 .changeset/funny-apples-pay.md diff --git a/.changeset/four-bottles-collect.md b/.changeset/four-bottles-collect.md deleted file mode 100644 index a62333fcda..0000000000 --- a/.changeset/four-bottles-collect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@firebase/data-connect': minor ---- - -Add validateArgsWithOptions() diff --git a/.changeset/funny-apples-pay.md b/.changeset/funny-apples-pay.md new file mode 100644 index 0000000000..ab820050d8 --- /dev/null +++ b/.changeset/funny-apples-pay.md @@ -0,0 +1,6 @@ +--- +'@firebase/data-connect': minor +'firebase': minor +--- + +Add validateArgsWithOptions() to Data Connect From 4cc594b679a96899a7235f3f18e2c1f74a1da2f8 Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Mon, 9 Mar 2026 15:15:11 -0700 Subject: [PATCH 6/8] export validateArgs --- packages/data-connect/src/api/index.ts | 2 +- packages/data-connect/src/util/validateArgs.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/data-connect/src/api/index.ts b/packages/data-connect/src/api/index.ts index 1140e7ed9f..e6a37314d7 100644 --- a/packages/data-connect/src/api/index.ts +++ b/packages/data-connect/src/api/index.ts @@ -39,7 +39,7 @@ export * from './Reference'; export * from './Mutation'; export * from './query'; export { setLogLevel } from '../logger'; -export { validateArgs } from '../util/validateArgs'; +export { validateArgs, validateArgsWithOptions } from '../util/validateArgs'; export { DataConnectErrorCode, Code, diff --git a/packages/data-connect/src/util/validateArgs.ts b/packages/data-connect/src/util/validateArgs.ts index 03304fc426..4058a0970f 100644 --- a/packages/data-connect/src/util/validateArgs.ts +++ b/packages/data-connect/src/util/validateArgs.ts @@ -124,5 +124,13 @@ export function validateArgsWithOptions( throw new DataConnectError(Code.INVALID_ARGUMENT, 'Variables required.'); } + // eslint-disable-next-line no-console + console.log( + `${dcFirstArg ? 'dcFirstArg' : 'firstArg'}: ${ + dcFirstArg ? dcFirstArg : dcOrVarsOrOptions + }, secondArg: ${varsOrOptions}, thirdArg: ${options}, hasVars: ${hasVars}, variablesRequired: ${variablesRequired}\n`, + `realVars: ${realVars}, realOptions: ${realOptions}` + ); + return { dc: dcInstance, vars: realVars, options: realOptions }; } From 835816c6debe0f986e317973e6a42b3b18751b7b Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Tue, 10 Mar 2026 13:52:12 -0700 Subject: [PATCH 7/8] remove vestigial console log --- packages/data-connect/src/util/validateArgs.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/data-connect/src/util/validateArgs.ts b/packages/data-connect/src/util/validateArgs.ts index 4058a0970f..03304fc426 100644 --- a/packages/data-connect/src/util/validateArgs.ts +++ b/packages/data-connect/src/util/validateArgs.ts @@ -124,13 +124,5 @@ export function validateArgsWithOptions( throw new DataConnectError(Code.INVALID_ARGUMENT, 'Variables required.'); } - // eslint-disable-next-line no-console - console.log( - `${dcFirstArg ? 'dcFirstArg' : 'firstArg'}: ${ - dcFirstArg ? dcFirstArg : dcOrVarsOrOptions - }, secondArg: ${varsOrOptions}, thirdArg: ${options}, hasVars: ${hasVars}, variablesRequired: ${variablesRequired}\n`, - `realVars: ${realVars}, realOptions: ${realOptions}` - ); - return { dc: dcInstance, vars: realVars, options: realOptions }; } From 11f9b88c6547e924218f9f0b0d5466a41d137955 Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Tue, 17 Mar 2026 07:27:57 -0700 Subject: [PATCH 8/8] update changeset --- .changeset/funny-apples-pay.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/funny-apples-pay.md b/.changeset/funny-apples-pay.md index ab820050d8..33231c4b21 100644 --- a/.changeset/funny-apples-pay.md +++ b/.changeset/funny-apples-pay.md @@ -3,4 +3,4 @@ 'firebase': minor --- -Add validateArgsWithOptions() to Data Connect +Add validateArgsWithOptions() to Data Connect (internal change, no need to add to public release notes). \ No newline at end of file