Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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