Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(react/fdc) add validateReactArgs for parsing arguments to variadic generated SDK query hook function signatures #174

Merged
merged 12 commits into from
Mar 25, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dataconnect-sdk/js/default-connector/index.cjs.js
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ exports.createMovieRef = function createMovieRef(dcOrVars, vars) {
exports.createMovie = function createMovie(dcOrVars, vars) {
return executeMutation(createMovieRef(dcOrVars, vars));
};

exports.upsertMovieRef = function upsertMovieRef(dcOrVars, vars) {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
@@ -23,6 +24,7 @@ exports.upsertMovieRef = function upsertMovieRef(dcOrVars, vars) {
exports.upsertMovie = function upsertMovie(dcOrVars, vars) {
return executeMutation(upsertMovieRef(dcOrVars, vars));
};

exports.deleteMovieRef = function deleteMovieRef(dcOrVars, vars) {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
@@ -31,6 +33,7 @@ exports.deleteMovieRef = function deleteMovieRef(dcOrVars, vars) {
exports.deleteMovie = function deleteMovie(dcOrVars, vars) {
return executeMutation(deleteMovieRef(dcOrVars, vars));
};

exports.addMetaRef = function addMetaRef(dc) {
const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined);
dcInstance._useGeneratedSdk();
@@ -39,6 +42,7 @@ exports.addMetaRef = function addMetaRef(dc) {
exports.addMeta = function addMeta(dc) {
return executeMutation(addMetaRef(dc));
};

exports.deleteMetaRef = function deleteMetaRef(dcOrVars, vars) {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
@@ -47,6 +51,7 @@ exports.deleteMetaRef = function deleteMetaRef(dcOrVars, vars) {
exports.deleteMeta = function deleteMeta(dcOrVars, vars) {
return executeMutation(deleteMetaRef(dcOrVars, vars));
};

exports.listMoviesRef = function listMoviesRef(dc) {
const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined);
dcInstance._useGeneratedSdk();
@@ -55,6 +60,7 @@ exports.listMoviesRef = function listMoviesRef(dc) {
exports.listMovies = function listMovies(dc) {
return executeQuery(listMoviesRef(dc));
};

exports.getMovieByIdRef = function getMovieByIdRef(dcOrVars, vars) {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
3 changes: 2 additions & 1 deletion packages/react/src/data-connect/index.ts
Original file line number Diff line number Diff line change
@@ -11,4 +11,5 @@ export {
useDataConnectMutation,
type useDataConnectMutationOptions,
} from "./useDataConnectMutation";
export type { QueryResultRequiredRef, UseDataConnectMutationResult, UseDataConnectQueryResult } from "./types";
export { validateReactArgs } from "./validateReactArgs";
export type { QueryResultRequiredRef, UseDataConnectMutationResult, UseDataConnectQueryResult } from "./types";
256 changes: 256 additions & 0 deletions packages/react/src/data-connect/validateReactArgs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { describe, expect, test } from "vitest";
import { validateReactArgs } from "./validateReactArgs";
import { connectorConfig } from "@/dataconnect/default-connector";
import { getDataConnect } from "firebase/data-connect";
import { firebaseApp } from "~/testing-utils";

// initialize firebase app
firebaseApp;

describe("validateReactArgs", () => {
const dataConnect = getDataConnect(connectorConfig);

const emptyObjectVars = {};
const nonEmptyVars = { limit: 5 };

const options = { meta: { hasOptions: true } };

test.each([
{
argsDescription: "no args are provided",
dcOrOptions: undefined,
options: undefined,
expectedInputVars: undefined,
expectedInputOpts: undefined,
},
{
argsDescription: "only dataconnect is provided",
dcOrOptions: dataConnect,
options: undefined,
expectedInputVars: undefined,
expectedInputOpts: undefined,
},
{
argsDescription: "only options are provided",
dcOrOptions: options,
options: undefined,
expectedInputVars: undefined,
expectedInputOpts: options,
},
{
argsDescription: "dataconnect and options are provided",
dcOrOptions: dataConnect,
options: options,
expectedInputVars: undefined,
expectedInputOpts: options,
},
])(
"parses args correctly when $argsDescription for an operation with no variables",
({ dcOrOptions, options, expectedInputVars, expectedInputOpts }) => {
const {
dc: dcInstance,
vars: inputVars,
options: inputOpts,
} = validateReactArgs(
connectorConfig,
dcOrOptions,
options
// hasVars = undefined (false-y)
// validateArgs = undefined (false-y)
);

expect(dcInstance).toBe(dataConnect);
expect(inputVars).toBe(expectedInputVars);
expect(inputOpts).toBe(expectedInputOpts);
}
);

test.each([
{
argsDescription: "no args are provided",
dcOrVarsOrOptions: undefined,
varsOrOptions: undefined,
options: undefined,
expectedInputVars: undefined,
expectedInputOpts: undefined,
},
{
argsDescription: "only dataconnect is provided",
dcOrVarsOrOptions: dataConnect,
varsOrOptions: undefined,
options: undefined,
expectedInputVars: undefined,
expectedInputOpts: undefined,
},
{
argsDescription: "only an empty vars object is provided",
dcOrVarsOrOptions: emptyObjectVars,
varsOrOptions: undefined,
options: undefined,
expectedInputVars: emptyObjectVars,
expectedInputOpts: undefined,
},
{
argsDescription: "only vars are provided",
dcOrVarsOrOptions: nonEmptyVars,
varsOrOptions: undefined,
options: undefined,
expectedInputVars: nonEmptyVars,
expectedInputOpts: undefined,
},
{
argsDescription: "only options are provided",
dcOrVarsOrOptions: undefined,
varsOrOptions: options,
options: undefined,
expectedInputVars: undefined,
expectedInputOpts: options,
},
{
argsDescription: "dataconnect and vars are provided",
dcOrVarsOrOptions: dataConnect,
varsOrOptions: nonEmptyVars,
options: undefined,
expectedInputVars: nonEmptyVars,
expectedInputOpts: undefined,
},
{
argsDescription: "dataconnect and options are provided",
dcOrVarsOrOptions: dataConnect,
varsOrOptions: undefined,
options: options,
expectedInputVars: undefined,
expectedInputOpts: options,
},
{
argsDescription: "dataconnect and vars and options are provided",
dcOrVarsOrOptions: dataConnect,
varsOrOptions: nonEmptyVars,
options: options,
expectedInputVars: nonEmptyVars,
expectedInputOpts: options,
},
])(
"parses args correctly when $argsDescription for an operation with all optional variables",
({
dcOrVarsOrOptions,
varsOrOptions,
options,
expectedInputVars,
expectedInputOpts,
}) => {
const {
dc: dcInstance,
vars: inputVars,
options: inputOpts,
} = validateReactArgs(
connectorConfig,
dcOrVarsOrOptions,
varsOrOptions,
options,
true, // hasVars = true
false // validateArgs = false
);

expect(dcInstance).toBe(dataConnect);
expect(inputVars).toBe(expectedInputVars);
expect(inputOpts).toBe(expectedInputOpts);
}
);

test.each([
{
argsDescription: "only vars are provided",
dcOrVarsOrOptions: nonEmptyVars,
varsOrOptions: undefined,
options: undefined,
expectedInputVars: nonEmptyVars,
expectedInputOpts: undefined,
},
{
argsDescription: "dataconnect and vars are provided",
dcOrVarsOrOptions: dataConnect,
varsOrOptions: nonEmptyVars,
options: undefined,
expectedInputVars: nonEmptyVars,
expectedInputOpts: undefined,
},
{
argsDescription: "vars and options are provided",
dcOrVarsOrOptions: nonEmptyVars,
varsOrOptions: options,
options: undefined,
expectedInputVars: nonEmptyVars,
expectedInputOpts: options,
},
{
argsDescription: "dataconnect and vars and options are provided",
dcOrVarsOrOptions: dataConnect,
varsOrOptions: nonEmptyVars,
options: options,
expectedInputVars: nonEmptyVars,
expectedInputOpts: options,
},
])(
"parses args correctly when $argsDescription for an operation with any required variables",
({
dcOrVarsOrOptions,
varsOrOptions,
options,
expectedInputVars,
expectedInputOpts,
}) => {
const {
dc: dcInstance,
vars: inputVars,
options: inputOpts,
} = validateReactArgs(
connectorConfig,
dcOrVarsOrOptions,
varsOrOptions,
options,
true, // hasVars = true
true // validateArgs = true
);

expect(dcInstance).toBe(dataConnect);
expect(inputVars).toBe(expectedInputVars);
expect(inputOpts).toBe(expectedInputOpts);
}
);

test.each([
{
argsDescription: "only dataconnect is provided",
dcOrVarsOrOptions: dataConnect,
varsOrOptions: undefined,
options: undefined,
},
{
argsDescription: "only options are provided",
dcOrVarsOrOptions: undefined,
varsOrOptions: options,
options: undefined,
},
{
argsDescription: "only dataconnect and options are provided",
dcOrVarsOrOptions: dataConnect,
varsOrOptions: undefined,
options: options,
},
])(
"throws error when $argsDescription for an operation with any required variables",
({ dcOrVarsOrOptions, varsOrOptions, options }) => {
expect(() => {
validateReactArgs(
connectorConfig,
dcOrVarsOrOptions,
varsOrOptions,
options,
true, // hasVars = true
true // validateArgs = true
);
}).toThrowError("invalid-argument: Variables required.");
}
);
});
68 changes: 68 additions & 0 deletions packages/react/src/data-connect/validateReactArgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
ConnectorConfig,
DataConnect,
getDataConnect,
} from "firebase/data-connect";
import { useDataConnectQueryOptions } from "./useDataConnectQuery";

type DataConnectOptions =
| useDataConnectQueryOptions
| useDataConnectQueryOptions;

interface ParsedReactArgs<Variables> {
dc: DataConnect;
vars: Variables;
options: DataConnectOptions;
}

/**
* The generated React SDK will allow the user to pass in variables, a Data Connect instance, or operation options.
* The only required argument is the variables, which are only required when the operation has at least one required
* variable. Otherwise, all arguments are optional. This function validates the variables and returns back the DataConnect
* instance, variables, and options based on the arguments passed in.
* @param connectorConfig DataConnect connector config
* @param dcOrVarsOrOptions the first argument provided to a generated react function
* @param varsOrOptions the second argument provided to a generated react function
* @param options the third argument provided to a generated react function
* @param hasVars boolean parameter indicating whether the operation has variables
* @param validateVars boolean parameter indicating whether we should expect to find a value for realVars
* @returns parsed DataConnect, Variables, and Options for the operation
* @internal
*/
export function validateReactArgs<Variables extends object>(
connectorConfig: ConnectorConfig,
dcOrVarsOrOptions?: DataConnect | Variables | DataConnectOptions,
varsOrOptions?: Variables | DataConnectOptions,
options?: DataConnectOptions,
hasVars?: boolean,
validateVars?: boolean
): ParsedReactArgs<Variables> {
let dcInstance: DataConnect;
let realVars: Variables;
let realOptions: DataConnectOptions;

if (dcOrVarsOrOptions && "enableEmulator" in dcOrVarsOrOptions) {
dcInstance = dcOrVarsOrOptions as DataConnect;
if (hasVars) {
realVars = varsOrOptions as Variables;
realOptions = options as DataConnectOptions;
} else {
realVars = undefined as unknown as Variables;
realOptions = varsOrOptions as DataConnectOptions;
}
} else {
dcInstance = getDataConnect(connectorConfig);
if (hasVars) {
realVars = dcOrVarsOrOptions as Variables;
realOptions = varsOrOptions as DataConnectOptions;
} else {
realVars = undefined as unknown as Variables;
realOptions = dcOrVarsOrOptions as DataConnectOptions;
}
}

if (!dcInstance || (!realVars && validateVars)) {
throw new Error("invalid-argument: Variables required."); // copied from firebase error codes
}
return { dc: dcInstance, vars: realVars, options: realOptions };
}