Skip to content

Commit 042904c

Browse files
committed
Review and update all prop value validation for accuracy
1 parent 3283f3e commit 042904c

File tree

4 files changed

+216
-110
lines changed

4 files changed

+216
-110
lines changed

packages/connect-react/examples/nextjs/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/connect-react/src/hooks/form-context.tsx

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import type {
99
import { useFrontendClient } from "./frontend-client-context";
1010
import type { ComponentFormProps } from "../components/ComponentForm";
1111
import type { FormFieldContext } from "./form-field-context";
12-
import { appPropError } from "./use-app";
12+
import {
13+
appPropErrors, arrayPropErrors, booleanPropErrors, integerPropErrors,
14+
stringPropErrors,
15+
} from "../utils/component";
1316

1417
export type DynamicProps<T extends ConfigurableProps> = { id: string; configurableProps: T; }; // TODO
1518

@@ -19,6 +22,7 @@ export type FormContext<T extends ConfigurableProps> = {
1922
configuredProps: ConfiguredProps<T>;
2023
dynamicProps?: DynamicProps<T>; // lots of calls require dynamicProps?.id, so need to expose
2124
dynamicPropsQueryIsFetching?: boolean;
25+
errors: Record<string, string[]>;
2226
fields: Record<string, FormFieldContext<ConfigurableProp>>;
2327
id: string;
2428
isValid: boolean;
@@ -168,55 +172,44 @@ export const FormContextProvider = <T extends ConfigurableProps>({
168172
// so can't rely on that base control form validation
169173
const propErrors = (prop: ConfigurableProp, value: unknown): string[] => {
170174
const errs: string[] = [];
171-
if (value === undefined) {
172-
if (!prop.optional) {
173-
errs.push("required");
174-
}
175-
} else if (prop.type === "integer") { // XXX type should be "number"? we don't support floats otherwise...
176-
if (typeof value !== "number") {
177-
errs.push("not a number");
178-
} else {
179-
if (prop.min != null && value < prop.min) {
180-
errs.push("number too small");
181-
}
182-
if (prop.max != null && value > prop.max) {
183-
errs.push("number too big");
184-
}
185-
}
186-
} else if (prop.type === "boolean") {
187-
if (typeof value !== "boolean") {
188-
errs.push("not a boolean");
189-
}
190-
} else if (prop.type === "string") {
191-
type StringProp = ConfigurableProp & {
192-
min?: number;
193-
max?: number;
194-
}
195-
const {
196-
min = 1, max,
197-
} = prop as StringProp;
198-
if (typeof value !== "string") {
199-
errs.push("not a string");
200-
} else {
201-
if (value.length < min) {
202-
errs.push(`string length must be at least ${min} characters`);
203-
}
204-
if (max && value.length > max) {
205-
errs.push(`string length must not exceed ${max} characters`);
206-
}
207-
}
208-
} else if (prop.type === "app") {
175+
if (prop.optional || prop.hidden || prop.disabled) return []
176+
if (typeof value === "undefined") {
177+
return [
178+
"required",
179+
]
180+
}
181+
if (prop.type === "app") {
209182
const field = fields[prop.name]
210183
if (field) {
211184
const app = field.extra.app
212-
const err = appPropError({
185+
errs.push(...(appPropErrors({
186+
prop,
213187
value,
214188
app,
215-
})
216-
if (err) errs.push(err)
189+
}) ?? []))
217190
} else {
218191
errs.push("field not registered")
219192
}
193+
} else if (prop.type === "boolean") {
194+
errs.push(...(booleanPropErrors({
195+
prop,
196+
value,
197+
}) ?? []))
198+
} else if (prop.type === "integer") {
199+
errs.push(...(integerPropErrors({
200+
prop,
201+
value,
202+
}) ?? []))
203+
} else if (prop.type === "string") {
204+
errs.push(...(stringPropErrors({
205+
prop,
206+
value,
207+
}) ?? []))
208+
} else if (prop.type === "string[]") {
209+
errs.push(...(arrayPropErrors({
210+
prop,
211+
value,
212+
}) ?? []))
220213
}
221214
return errs;
222215
};
@@ -377,6 +370,7 @@ export const FormContextProvider = <T extends ConfigurableProps>({
377370
configuredProps,
378371
dynamicProps,
379372
dynamicPropsQueryIsFetching,
373+
errors,
380374
fields,
381375
optionalPropIsEnabled,
382376
optionalPropSetEnabled,
Lines changed: 2 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import {
22
useQuery, type UseQueryOptions,
33
} from "@tanstack/react-query";
4+
import type { GetAppResponse } from "@pipedream/sdk";
45
import { useFrontendClient } from "./frontend-client-context";
5-
import type {
6-
AppRequestResponse, AppResponse, ConfigurablePropApp,
7-
PropValue,
8-
} from "@pipedream/sdk";
96

107
/**
118
* Get details about an app
129
*/
13-
export const useApp = (slug: string, opts?:{ useQueryOpts?: Omit<UseQueryOptions<AppRequestResponse>, "queryKey" | "queryFn">;}) => {
10+
export const useApp = (slug: string, opts?:{ useQueryOpts?: Omit<UseQueryOptions<GetAppResponse>, "queryKey" | "queryFn">;}) => {
1411
const client = useFrontendClient();
1512
const query = useQuery({
1613
queryKey: [
@@ -26,65 +23,3 @@ export const useApp = (slug: string, opts?:{ useQueryOpts?: Omit<UseQueryOptions
2623
app: query.data?.data,
2724
};
2825
};
29-
30-
type AppResponseWithExtractedCustomFields = AppResponse & {
31-
extracted_custom_fields_names: string[]
32-
}
33-
34-
type AppCustomField = {
35-
name: string
36-
optional?: boolean
37-
}
38-
39-
type OauthAppPropValue = PropValue<"app"> & {
40-
oauth_access_token?: string
41-
}
42-
43-
function getCustomFields(app: AppResponse): AppCustomField[] {
44-
const isOauth = app.auth_type === "oauth"
45-
const userDefinedCustomFields = JSON.parse(app.custom_fields_json || "[]")
46-
if ("extracted_custom_fields_names" in app && app.extracted_custom_fields_names) {
47-
const extractedCustomFields = ((app as AppResponseWithExtractedCustomFields).extracted_custom_fields_names || []).map(
48-
(name) => ({
49-
name,
50-
}),
51-
)
52-
userDefinedCustomFields.push(...extractedCustomFields)
53-
}
54-
return userDefinedCustomFields.map((cf: AppCustomField) => {
55-
return {
56-
...cf,
57-
// if oauth, treat all as optional (they are usually needed for getting access token)
58-
optional: cf.optional || isOauth,
59-
}
60-
})
61-
}
62-
63-
export function appPropError(opts: { value: any, app: AppResponse | undefined }): string | undefined {
64-
const { app, value } = opts
65-
if (!app) {
66-
return "app field not registered"
67-
}
68-
if (!value) {
69-
return "no app configured"
70-
}
71-
if (typeof value !== "object") {
72-
return "not an app"
73-
}
74-
const _value = value as PropValue<"app">
75-
if ("authProvisionId" in _value && !_value.authProvisionId) {
76-
if (app.auth_type) {
77-
if (app.auth_type === "oauth" && !(_value as OauthAppPropValue).oauth_access_token) {
78-
return "missing oauth token"
79-
}
80-
if (app.auth_type === "oauth" || app.auth_type === "keys") {
81-
for (const cf of getCustomFields(app)) {
82-
if (!cf.optional && !_value[cf.name]) {
83-
return "missing custom field"
84-
}
85-
}
86-
}
87-
return "no auth provision configured"
88-
}
89-
}
90-
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import type {
2+
App, ConfigurableProp, ConfigurablePropApp, ConfigurablePropBoolean, ConfigurablePropInteger, ConfigurablePropString, ConfigurablePropStringArray, PropValue,
3+
} from "@pipedream/sdk";
4+
5+
export type PropOptionValue<T> = {
6+
__lv: {
7+
value: T
8+
}
9+
}
10+
11+
export function valueFromOption<T>(value: T | PropOptionValue<T>): T | undefined | null {
12+
if (typeof value === "object" && value && "__lv" in value) {
13+
return (value as PropOptionValue<T>).__lv.value
14+
}
15+
return value
16+
}
17+
18+
export type PropOption<T> = {
19+
emitValue: T | PropOptionValue<T>
20+
}
21+
export type PropOptions<T> = {
22+
selectedOptions: PropOption<T>[]
23+
}
24+
25+
export function valuesFromOptions<T>(value: unknown | T[] | PropOptions<T>): T[] {
26+
if (typeof value === "object" && value && "selectedOptions" in value && Array.isArray(value.selectedOptions)) {
27+
const selectedOptions = value.selectedOptions as PropOption<T>[]
28+
const results = []
29+
for (const so of selectedOptions) {
30+
if (typeof so === "object" && so && "emitValue" in so) {
31+
const emitValue = so.emitValue as T | PropOptionValue<T>
32+
if (typeof emitValue === "object" && emitValue && "__lv" in emitValue) {
33+
results.push(emitValue.__lv.value)
34+
}
35+
results.push(emitValue)
36+
}
37+
throw "unexpected value"
38+
}
39+
}
40+
if (!Array.isArray(value))
41+
throw "unexpected value"
42+
return value as T[]
43+
}
44+
45+
export type ValidationOpts<T extends ConfigurableProp> = {
46+
prop: T
47+
value: unknown
48+
app?: App
49+
}
50+
51+
export function arrayPropErrors(opts: ValidationOpts<ConfigurablePropStringArray>): string[] | undefined {
52+
const _values = valuesFromOptions(opts.value)
53+
if (typeof _values === "undefined") {
54+
return [
55+
"required",
56+
]
57+
}
58+
if (Array.isArray(_values) && !_values.length) return [
59+
"empty array",
60+
]
61+
}
62+
63+
export function booleanPropErrors(opts: ValidationOpts<ConfigurablePropBoolean>): string[] | undefined {
64+
const _value = valueFromOption(opts.value)
65+
if (_value == null || typeof _value === "undefined") return [
66+
"required",
67+
]
68+
}
69+
70+
export function integerPropErrors(opts: ValidationOpts<ConfigurablePropInteger>): string[] | undefined {
71+
const {
72+
prop, value: valueOpt,
73+
} = opts
74+
const value = valueFromOption(valueOpt)
75+
76+
if (value == null || typeof value === "undefined") return [
77+
"required",
78+
]
79+
80+
const _value: number = typeof value === "number"
81+
? value
82+
: parseInt(String(value))
83+
84+
if (_value !== _value) return [
85+
"not a number",
86+
] // NaN
87+
const errs = []
88+
if (typeof prop.min === "number" && _value < prop.min) errs.push("number too small")
89+
if (typeof prop.max === "number" && _value > prop.max) errs.push("number too big")
90+
return errs
91+
}
92+
93+
export function stringPropErrors(opts: ValidationOpts<ConfigurablePropString>): string[] | undefined {
94+
const _value = valueFromOption(opts.value)
95+
if (!_value) return [
96+
"string must not be empty",
97+
]
98+
}
99+
100+
type AppWithExtractedCustomFields = App & {
101+
extracted_custom_fields_names: string[]
102+
}
103+
104+
type AppCustomField = {
105+
name: string
106+
optional?: boolean
107+
}
108+
109+
type OauthAppPropValue = PropValue<"app"> & {
110+
oauth_access_token: string
111+
}
112+
113+
type AppPropValueWithCustomFields<T extends AppCustomField[]> = PropValue<"app"> & {
114+
[K in T[number]["name"]]: T[number]
115+
}
116+
117+
function getCustomFields(app: App): AppCustomField[] {
118+
const isOauth = app.auth_type === "oauth"
119+
const userDefinedCustomFields = JSON.parse(app.custom_fields_json || "[]")
120+
if ("extracted_custom_fields_names" in app && app.extracted_custom_fields_names) {
121+
const extractedCustomFields = ((app as AppWithExtractedCustomFields).extracted_custom_fields_names || []).map(
122+
(name) => ({
123+
name,
124+
}),
125+
)
126+
userDefinedCustomFields.push(...extractedCustomFields)
127+
}
128+
return userDefinedCustomFields.map((cf: AppCustomField) => {
129+
return {
130+
...cf,
131+
// if oauth, treat all as optional (they are usually needed for getting access token)
132+
optional: cf.optional || isOauth,
133+
}
134+
})
135+
}
136+
137+
export function appPropErrors(opts: ValidationOpts<ConfigurablePropApp>): string[] | undefined {
138+
const {
139+
app, value,
140+
} = opts
141+
if (!app) {
142+
return [
143+
"app field not registered",
144+
]
145+
}
146+
if (!value) {
147+
return [
148+
"no app configured",
149+
]
150+
}
151+
if (typeof value !== "object") {
152+
return [
153+
"not an app",
154+
]
155+
}
156+
const _value = value as PropValue<"app">
157+
if ("authProvisionId" in _value && !_value.authProvisionId) {
158+
if (app.auth_type) {
159+
const errs = []
160+
if (app.auth_type === "oauth" && !(_value as OauthAppPropValue).oauth_access_token) {
161+
errs.push("missing oauth token")
162+
}
163+
if (app.auth_type === "oauth" || app.auth_type === "keys") {
164+
const customFields = getCustomFields(app)
165+
const _valueWithCustomFields = _value as AppPropValueWithCustomFields<typeof customFields>
166+
for (const cf of customFields) {
167+
if (!cf.optional && !_valueWithCustomFields[cf.name]) {
168+
errs.push(`missing custom field: ${cf.name}`)
169+
}
170+
}
171+
}
172+
if (app.auth_type !== "none")
173+
errs.push("no auth provision configured")
174+
return errs
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)