diff --git a/.changeset/funny-apples-pay.md b/.changeset/funny-apples-pay.md new file mode 100644 index 0000000000..33231c4b21 --- /dev/null +++ b/.changeset/funny-apples-pay.md @@ -0,0 +1,6 @@ +--- +'@firebase/data-connect': minor +'firebase': minor +--- + +Add validateArgsWithOptions() to Data Connect (internal change, no need to add to public release notes). \ No newline at end of file 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/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/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..03304fc426 100644 --- a/packages/data-connect/src/util/validateArgs.ts +++ b/packages/data-connect/src/util/validateArgs.ts @@ -15,24 +15,32 @@ * 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. 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 validateVars + * @param variablesRequired * @returns {DataConnect} and {Variables} instance * @internal */ @@ -40,19 +48,81 @@ export function validateArgs( connectorConfig: ConnectorConfig, dcOrVars?: DataConnect | Variables, vars?: Variables, - validateVars?: boolean + variablesRequired?: boolean ): ParsedArgs { let dcInstance: DataConnect; let realVars: Variables; - if (dcOrVars && 'enableEmulator' in dcOrVars) { + + 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 && validateVars)) { + + 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 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 options + * @returns {DataConnect} and {Variables} instance, and optionally {ExecuteQueryOptions} + * @internal + */ +export function validateArgsWithOptions( + connectorConfig: ConnectorConfig, + dcOrVarsOrOptions?: DataConnect | Variables | ExecuteQueryOptions, + varsOrOptions?: Variables | ExecuteQueryOptions, + options?: ExecuteQueryOptions, + hasVars?: boolean, + variablesRequired?: boolean +): ParsedArgs { + let dcInstance: DataConnect; + let realVars: Variables; + let realOptions: ExecuteQueryOptions; + + const dcFirstArg = dcOrVarsOrOptions && 'enableEmulator' in dcOrVarsOrOptions; + + 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 }; +} 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..05b66f2431 --- /dev/null +++ b/packages/data-connect/test/unit/validateArgs.test.ts @@ -0,0 +1,400 @@ +/** + * @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'; + +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, + validateArgsWithOptions +} 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('validateArgs', () => { + 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('validateArgsWithOptions', () => { + describe('should parse arguments properly', () => { + 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, + 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, + 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, + 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 (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 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; + }); + }); + + 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; + }); + }); + }); + + describe('should throw when vars are required but missing', () => { + it('and only dc is provided', () => { + expect(() => { + validateArgsWithOptions( + connectorConfig, + 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( + connectorConfig, + 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( + connectorConfig, + 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( + connectorConfig, + undefined, + undefined, + undefined, + /** hasVars = */ true, + /** variablesRequired = */ true + ); + }) + .to.throw(DataConnectError, 'Variables required') + .with.property('code', Code.INVALID_ARGUMENT); + }); + }); + }); +});