Skip to content
Merged
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions packages/connect-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<!-- markdownlint-disable MD024 -->
# Changelog

# [1.0.0-preview.11] - 2024-12-13

- Make prop validation more consistent with app behavior
- Relax validation of string props when value is not a string

# [1.0.0-preview.10] - 2024-12-12

- Enforce string length limits
Expand Down
2 changes: 1 addition & 1 deletion packages/connect-react/examples/nextjs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/connect-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/connect-react",
"version": "1.0.0-preview.10",
"version": "1.0.0-preview.11",
"description": "Pipedream Connect library for React",
"files": [
"dist"
Expand Down
73 changes: 31 additions & 42 deletions packages/connect-react/src/hooks/form-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type {
import { useFrontendClient } from "./frontend-client-context";
import type { ComponentFormProps } from "../components/ComponentForm";
import type { FormFieldContext } from "./form-field-context";
import { appPropError } from "./use-app";
import {
appPropErrors, arrayPropErrors, booleanPropErrors, integerPropErrors,
stringPropErrors,
} from "../utils/component";

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

Expand All @@ -19,6 +22,7 @@ export type FormContext<T extends ConfigurableProps> = {
configuredProps: ConfiguredProps<T>;
dynamicProps?: DynamicProps<T>; // lots of calls require dynamicProps?.id, so need to expose
dynamicPropsQueryIsFetching?: boolean;
errors: Record<string, string[]>;
fields: Record<string, FormFieldContext<ConfigurableProp>>;
id: string;
isValid: boolean;
Expand Down Expand Up @@ -168,55 +172,39 @@ export const FormContextProvider = <T extends ConfigurableProps>({
// so can't rely on that base control form validation
const propErrors = (prop: ConfigurableProp, value: unknown): string[] => {
const errs: string[] = [];
if (value === undefined) {
if (!prop.optional) {
errs.push("required");
}
} else if (prop.type === "integer") { // XXX type should be "number"? we don't support floats otherwise...
if (typeof value !== "number") {
errs.push("not a number");
} else {
if (prop.min != null && value < prop.min) {
errs.push("number too small");
}
if (prop.max != null && value > prop.max) {
errs.push("number too big");
}
}
} else if (prop.type === "boolean") {
if (typeof value !== "boolean") {
errs.push("not a boolean");
}
} else if (prop.type === "string") {
type StringProp = ConfigurableProp & {
min?: number;
max?: number;
}
const {
min = 1, max,
} = prop as StringProp;
if (typeof value !== "string") {
errs.push("not a string");
} else {
if (value.length < min) {
errs.push(`string length must be at least ${min} characters`);
}
if (max && value.length > max) {
errs.push(`string length must not exceed ${max} characters`);
}
}
} else if (prop.type === "app") {
if (prop.optional || prop.hidden || prop.disabled) return []
if (prop.type === "app") {
const field = fields[prop.name]
if (field) {
const app = field.extra.app
const err = appPropError({
errs.push(...(appPropErrors({
prop,
value,
app,
})
if (err) errs.push(err)
}) ?? []))
} else {
errs.push("field not registered")
}
} else if (prop.type === "boolean") {
errs.push(...(booleanPropErrors({
prop,
value,
}) ?? []))
} else if (prop.type === "integer") {
errs.push(...(integerPropErrors({
prop,
value,
}) ?? []))
} else if (prop.type === "string") {
errs.push(...(stringPropErrors({
prop,
value,
}) ?? []))
} else if (prop.type === "string[]") {
errs.push(...(arrayPropErrors({
prop,
value,
}) ?? []))
}
return errs;
};
Expand Down Expand Up @@ -377,6 +365,7 @@ export const FormContextProvider = <T extends ConfigurableProps>({
configuredProps,
dynamicProps,
dynamicPropsQueryIsFetching,
errors,
fields,
optionalPropIsEnabled,
optionalPropSetEnabled,
Expand Down
69 changes: 2 additions & 67 deletions packages/connect-react/src/hooks/use-app.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import {
useQuery, type UseQueryOptions,
} from "@tanstack/react-query";
import type { GetAppResponse } from "@pipedream/sdk";
import { useFrontendClient } from "./frontend-client-context";
import type {
AppRequestResponse, AppResponse, ConfigurablePropApp,
PropValue,
} from "@pipedream/sdk";

/**
* Get details about an app
*/
export const useApp = (slug: string, opts?:{ useQueryOpts?: Omit<UseQueryOptions<AppRequestResponse>, "queryKey" | "queryFn">;}) => {
export const useApp = (slug: string, opts?:{ useQueryOpts?: Omit<UseQueryOptions<GetAppResponse>, "queryKey" | "queryFn">;}) => {
const client = useFrontendClient();
const query = useQuery({
queryKey: [
Expand All @@ -26,65 +23,3 @@ export const useApp = (slug: string, opts?:{ useQueryOpts?: Omit<UseQueryOptions
app: query.data?.data,
};
};

type AppResponseWithExtractedCustomFields = AppResponse & {
extracted_custom_fields_names: string[]
}

type AppCustomField = {
name: string
optional?: boolean
}

type OauthAppPropValue = PropValue<"app"> & {
oauth_access_token?: string
}

function getCustomFields(app: AppResponse): AppCustomField[] {
const isOauth = app.auth_type === "oauth"
const userDefinedCustomFields = JSON.parse(app.custom_fields_json || "[]")
if ("extracted_custom_fields_names" in app && app.extracted_custom_fields_names) {
const extractedCustomFields = ((app as AppResponseWithExtractedCustomFields).extracted_custom_fields_names || []).map(
(name) => ({
name,
}),
)
userDefinedCustomFields.push(...extractedCustomFields)
}
return userDefinedCustomFields.map((cf: AppCustomField) => {
return {
...cf,
// if oauth, treat all as optional (they are usually needed for getting access token)
optional: cf.optional || isOauth,
}
})
}

export function appPropError(opts: { value: any, app: AppResponse | undefined }): string | undefined {
const { app, value } = opts
if (!app) {
return "app field not registered"
}
if (!value) {
return "no app configured"
}
if (typeof value !== "object") {
return "not an app"
}
const _value = value as PropValue<"app">
if ("authProvisionId" in _value && !_value.authProvisionId) {
if (app.auth_type) {
if (app.auth_type === "oauth" && !(_value as OauthAppPropValue).oauth_access_token) {
return "missing oauth token"
}
if (app.auth_type === "oauth" || app.auth_type === "keys") {
for (const cf of getCustomFields(app)) {
if (!cf.optional && !_value[cf.name]) {
return "missing custom field"
}
}
}
return "no auth provision configured"
}
}
}
Loading
Loading