Skip to content

Commit 9587431

Browse files
stephenarosajEhesp
andauthored
feat(react): Add validateReactArgs for parsing arguments to variadic generated SDK query hook function signatures (#174)
* feat(react/fdc) add for parsing arguments to variadic generated SDK query hook function signatures * add unit tests for validateReactArgs * update unit tests for validateReactArgs * remove some changes unintentionally added to this PR, fix formatting * remove some changes unintentionally added to this PR * remove some changes unintentionally added to this PR * remove changes to pnpm-lock.yaml unintentionally added to this PR * remove changes to pnpm-lock.yaml unintentionally added to this PR * address review comments - collapse expect checks in testing, create new explicit DataConnectOptions type * run VSCode file formatter --------- Co-authored-by: Elliot Hesp <[email protected]>
1 parent d26ef77 commit 9587431

File tree

4 files changed

+332
-1
lines changed

4 files changed

+332
-1
lines changed

dataconnect-sdk/js/default-connector/index.cjs.js

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ exports.createMovieRef = function createMovieRef(dcOrVars, vars) {
1515
exports.createMovie = function createMovie(dcOrVars, vars) {
1616
return executeMutation(createMovieRef(dcOrVars, vars));
1717
};
18+
1819
exports.upsertMovieRef = function upsertMovieRef(dcOrVars, vars) {
1920
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
2021
dcInstance._useGeneratedSdk();
@@ -23,6 +24,7 @@ exports.upsertMovieRef = function upsertMovieRef(dcOrVars, vars) {
2324
exports.upsertMovie = function upsertMovie(dcOrVars, vars) {
2425
return executeMutation(upsertMovieRef(dcOrVars, vars));
2526
};
27+
2628
exports.deleteMovieRef = function deleteMovieRef(dcOrVars, vars) {
2729
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
2830
dcInstance._useGeneratedSdk();
@@ -31,6 +33,7 @@ exports.deleteMovieRef = function deleteMovieRef(dcOrVars, vars) {
3133
exports.deleteMovie = function deleteMovie(dcOrVars, vars) {
3234
return executeMutation(deleteMovieRef(dcOrVars, vars));
3335
};
36+
3437
exports.addMetaRef = function addMetaRef(dc) {
3538
const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined);
3639
dcInstance._useGeneratedSdk();
@@ -39,6 +42,7 @@ exports.addMetaRef = function addMetaRef(dc) {
3942
exports.addMeta = function addMeta(dc) {
4043
return executeMutation(addMetaRef(dc));
4144
};
45+
4246
exports.deleteMetaRef = function deleteMetaRef(dcOrVars, vars) {
4347
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
4448
dcInstance._useGeneratedSdk();
@@ -47,6 +51,7 @@ exports.deleteMetaRef = function deleteMetaRef(dcOrVars, vars) {
4751
exports.deleteMeta = function deleteMeta(dcOrVars, vars) {
4852
return executeMutation(deleteMetaRef(dcOrVars, vars));
4953
};
54+
5055
exports.listMoviesRef = function listMoviesRef(dc) {
5156
const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined);
5257
dcInstance._useGeneratedSdk();
@@ -55,6 +60,7 @@ exports.listMoviesRef = function listMoviesRef(dc) {
5560
exports.listMovies = function listMovies(dc) {
5661
return executeQuery(listMoviesRef(dc));
5762
};
63+
5864
exports.getMovieByIdRef = function getMovieByIdRef(dcOrVars, vars) {
5965
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
6066
dcInstance._useGeneratedSdk();

packages/react/src/data-connect/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export {
1111
useDataConnectMutation,
1212
type useDataConnectMutationOptions,
1313
} from "./useDataConnectMutation";
14-
export type { QueryResultRequiredRef, UseDataConnectMutationResult, UseDataConnectQueryResult } from "./types";
14+
export { validateReactArgs } from "./validateReactArgs";
15+
export type { QueryResultRequiredRef, UseDataConnectMutationResult, UseDataConnectQueryResult } from "./types";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import { describe, expect, test } from "vitest";
2+
import { validateReactArgs } from "./validateReactArgs";
3+
import { connectorConfig } from "@/dataconnect/default-connector";
4+
import { getDataConnect } from "firebase/data-connect";
5+
import { firebaseApp } from "~/testing-utils";
6+
7+
// initialize firebase app
8+
firebaseApp;
9+
10+
describe("validateReactArgs", () => {
11+
const dataConnect = getDataConnect(connectorConfig);
12+
13+
const emptyObjectVars = {};
14+
const nonEmptyVars = { limit: 5 };
15+
16+
const options = { meta: { hasOptions: true } };
17+
18+
test.each([
19+
{
20+
argsDescription: "no args are provided",
21+
dcOrOptions: undefined,
22+
options: undefined,
23+
expectedInputVars: undefined,
24+
expectedInputOpts: undefined,
25+
},
26+
{
27+
argsDescription: "only dataconnect is provided",
28+
dcOrOptions: dataConnect,
29+
options: undefined,
30+
expectedInputVars: undefined,
31+
expectedInputOpts: undefined,
32+
},
33+
{
34+
argsDescription: "only options are provided",
35+
dcOrOptions: options,
36+
options: undefined,
37+
expectedInputVars: undefined,
38+
expectedInputOpts: options,
39+
},
40+
{
41+
argsDescription: "dataconnect and options are provided",
42+
dcOrOptions: dataConnect,
43+
options: options,
44+
expectedInputVars: undefined,
45+
expectedInputOpts: options,
46+
},
47+
])(
48+
"parses args correctly when $argsDescription for an operation with no variables",
49+
({ dcOrOptions, options, expectedInputVars, expectedInputOpts }) => {
50+
const {
51+
dc: dcInstance,
52+
vars: inputVars,
53+
options: inputOpts,
54+
} = validateReactArgs(
55+
connectorConfig,
56+
dcOrOptions,
57+
options
58+
// hasVars = undefined (false-y)
59+
// validateArgs = undefined (false-y)
60+
);
61+
62+
expect(dcInstance).toBe(dataConnect);
63+
expect(inputVars).toBe(expectedInputVars);
64+
expect(inputOpts).toBe(expectedInputOpts);
65+
}
66+
);
67+
68+
test.each([
69+
{
70+
argsDescription: "no args are provided",
71+
dcOrVarsOrOptions: undefined,
72+
varsOrOptions: undefined,
73+
options: undefined,
74+
expectedInputVars: undefined,
75+
expectedInputOpts: undefined,
76+
},
77+
{
78+
argsDescription: "only dataconnect is provided",
79+
dcOrVarsOrOptions: dataConnect,
80+
varsOrOptions: undefined,
81+
options: undefined,
82+
expectedInputVars: undefined,
83+
expectedInputOpts: undefined,
84+
},
85+
{
86+
argsDescription: "only an empty vars object is provided",
87+
dcOrVarsOrOptions: emptyObjectVars,
88+
varsOrOptions: undefined,
89+
options: undefined,
90+
expectedInputVars: emptyObjectVars,
91+
expectedInputOpts: undefined,
92+
},
93+
{
94+
argsDescription: "only vars are provided",
95+
dcOrVarsOrOptions: nonEmptyVars,
96+
varsOrOptions: undefined,
97+
options: undefined,
98+
expectedInputVars: nonEmptyVars,
99+
expectedInputOpts: undefined,
100+
},
101+
{
102+
argsDescription: "only options are provided",
103+
dcOrVarsOrOptions: undefined,
104+
varsOrOptions: options,
105+
options: undefined,
106+
expectedInputVars: undefined,
107+
expectedInputOpts: options,
108+
},
109+
{
110+
argsDescription: "dataconnect and vars are provided",
111+
dcOrVarsOrOptions: dataConnect,
112+
varsOrOptions: nonEmptyVars,
113+
options: undefined,
114+
expectedInputVars: nonEmptyVars,
115+
expectedInputOpts: undefined,
116+
},
117+
{
118+
argsDescription: "dataconnect and options are provided",
119+
dcOrVarsOrOptions: dataConnect,
120+
varsOrOptions: undefined,
121+
options: options,
122+
expectedInputVars: undefined,
123+
expectedInputOpts: options,
124+
},
125+
{
126+
argsDescription: "dataconnect and vars and options are provided",
127+
dcOrVarsOrOptions: dataConnect,
128+
varsOrOptions: nonEmptyVars,
129+
options: options,
130+
expectedInputVars: nonEmptyVars,
131+
expectedInputOpts: options,
132+
},
133+
])(
134+
"parses args correctly when $argsDescription for an operation with all optional variables",
135+
({
136+
dcOrVarsOrOptions,
137+
varsOrOptions,
138+
options,
139+
expectedInputVars,
140+
expectedInputOpts,
141+
}) => {
142+
const {
143+
dc: dcInstance,
144+
vars: inputVars,
145+
options: inputOpts,
146+
} = validateReactArgs(
147+
connectorConfig,
148+
dcOrVarsOrOptions,
149+
varsOrOptions,
150+
options,
151+
true, // hasVars = true
152+
false // validateArgs = false
153+
);
154+
155+
expect(dcInstance).toBe(dataConnect);
156+
expect(inputVars).toBe(expectedInputVars);
157+
expect(inputOpts).toBe(expectedInputOpts);
158+
}
159+
);
160+
161+
test.each([
162+
{
163+
argsDescription: "only vars are provided",
164+
dcOrVarsOrOptions: nonEmptyVars,
165+
varsOrOptions: undefined,
166+
options: undefined,
167+
expectedInputVars: nonEmptyVars,
168+
expectedInputOpts: undefined,
169+
},
170+
{
171+
argsDescription: "dataconnect and vars are provided",
172+
dcOrVarsOrOptions: dataConnect,
173+
varsOrOptions: nonEmptyVars,
174+
options: undefined,
175+
expectedInputVars: nonEmptyVars,
176+
expectedInputOpts: undefined,
177+
},
178+
{
179+
argsDescription: "vars and options are provided",
180+
dcOrVarsOrOptions: nonEmptyVars,
181+
varsOrOptions: options,
182+
options: undefined,
183+
expectedInputVars: nonEmptyVars,
184+
expectedInputOpts: options,
185+
},
186+
{
187+
argsDescription: "dataconnect and vars and options are provided",
188+
dcOrVarsOrOptions: dataConnect,
189+
varsOrOptions: nonEmptyVars,
190+
options: options,
191+
expectedInputVars: nonEmptyVars,
192+
expectedInputOpts: options,
193+
},
194+
])(
195+
"parses args correctly when $argsDescription for an operation with any required variables",
196+
({
197+
dcOrVarsOrOptions,
198+
varsOrOptions,
199+
options,
200+
expectedInputVars,
201+
expectedInputOpts,
202+
}) => {
203+
const {
204+
dc: dcInstance,
205+
vars: inputVars,
206+
options: inputOpts,
207+
} = validateReactArgs(
208+
connectorConfig,
209+
dcOrVarsOrOptions,
210+
varsOrOptions,
211+
options,
212+
true, // hasVars = true
213+
true // validateArgs = true
214+
);
215+
216+
expect(dcInstance).toBe(dataConnect);
217+
expect(inputVars).toBe(expectedInputVars);
218+
expect(inputOpts).toBe(expectedInputOpts);
219+
}
220+
);
221+
222+
test.each([
223+
{
224+
argsDescription: "only dataconnect is provided",
225+
dcOrVarsOrOptions: dataConnect,
226+
varsOrOptions: undefined,
227+
options: undefined,
228+
},
229+
{
230+
argsDescription: "only options are provided",
231+
dcOrVarsOrOptions: undefined,
232+
varsOrOptions: options,
233+
options: undefined,
234+
},
235+
{
236+
argsDescription: "only dataconnect and options are provided",
237+
dcOrVarsOrOptions: dataConnect,
238+
varsOrOptions: undefined,
239+
options: options,
240+
},
241+
])(
242+
"throws error when $argsDescription for an operation with any required variables",
243+
({ dcOrVarsOrOptions, varsOrOptions, options }) => {
244+
expect(() => {
245+
validateReactArgs(
246+
connectorConfig,
247+
dcOrVarsOrOptions,
248+
varsOrOptions,
249+
options,
250+
true, // hasVars = true
251+
true // validateArgs = true
252+
);
253+
}).toThrowError("invalid-argument: Variables required.");
254+
}
255+
);
256+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
ConnectorConfig,
3+
DataConnect,
4+
getDataConnect,
5+
} from "firebase/data-connect";
6+
import { useDataConnectQueryOptions } from "./useDataConnectQuery";
7+
8+
type DataConnectOptions =
9+
| useDataConnectQueryOptions
10+
| useDataConnectQueryOptions;
11+
12+
interface ParsedReactArgs<Variables> {
13+
dc: DataConnect;
14+
vars: Variables;
15+
options: DataConnectOptions;
16+
}
17+
18+
/**
19+
* The generated React SDK will allow the user to pass in variables, a Data Connect instance, or operation options.
20+
* The only required argument is the variables, which are only required when the operation has at least one required
21+
* variable. Otherwise, all arguments are optional. This function validates the variables and returns back the DataConnect
22+
* instance, variables, and options based on the arguments passed in.
23+
* @param connectorConfig DataConnect connector config
24+
* @param dcOrVarsOrOptions the first argument provided to a generated react function
25+
* @param varsOrOptions the second argument provided to a generated react function
26+
* @param options the third argument provided to a generated react function
27+
* @param hasVars boolean parameter indicating whether the operation has variables
28+
* @param validateVars boolean parameter indicating whether we should expect to find a value for realVars
29+
* @returns parsed DataConnect, Variables, and Options for the operation
30+
* @internal
31+
*/
32+
export function validateReactArgs<Variables extends object>(
33+
connectorConfig: ConnectorConfig,
34+
dcOrVarsOrOptions?: DataConnect | Variables | DataConnectOptions,
35+
varsOrOptions?: Variables | DataConnectOptions,
36+
options?: DataConnectOptions,
37+
hasVars?: boolean,
38+
validateVars?: boolean
39+
): ParsedReactArgs<Variables> {
40+
let dcInstance: DataConnect;
41+
let realVars: Variables;
42+
let realOptions: DataConnectOptions;
43+
44+
if (dcOrVarsOrOptions && "enableEmulator" in dcOrVarsOrOptions) {
45+
dcInstance = dcOrVarsOrOptions as DataConnect;
46+
if (hasVars) {
47+
realVars = varsOrOptions as Variables;
48+
realOptions = options as DataConnectOptions;
49+
} else {
50+
realVars = undefined as unknown as Variables;
51+
realOptions = varsOrOptions as DataConnectOptions;
52+
}
53+
} else {
54+
dcInstance = getDataConnect(connectorConfig);
55+
if (hasVars) {
56+
realVars = dcOrVarsOrOptions as Variables;
57+
realOptions = varsOrOptions as DataConnectOptions;
58+
} else {
59+
realVars = undefined as unknown as Variables;
60+
realOptions = dcOrVarsOrOptions as DataConnectOptions;
61+
}
62+
}
63+
64+
if (!dcInstance || (!realVars && validateVars)) {
65+
throw new Error("invalid-argument: Variables required."); // copied from firebase error codes
66+
}
67+
return { dc: dcInstance, vars: realVars, options: realOptions };
68+
}

0 commit comments

Comments
 (0)