This commit is contained in:
@@ -72,6 +72,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@effect/schema": "^0.72.0",
|
"@effect/schema": "^0.72.0",
|
||||||
"@prisma/studio-server": "^0.502.0",
|
"@prisma/studio-server": "^0.502.0",
|
||||||
|
"@tanstack/form-core": "^0.30.0",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"bun-types": "^1.1.26",
|
"bun-types": "^1.1.26",
|
||||||
"effect": "^3.7.0",
|
"effect": "^3.7.0",
|
||||||
|
|||||||
138
src/Schema/TanStackForm/makeSchemaFormValidator.ts
Normal file
138
src/Schema/TanStackForm/makeSchemaFormValidator.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { ArrayFormatter, Schema } from "@effect/schema"
|
||||||
|
import type { ParseOptions } from "@effect/schema/AST"
|
||||||
|
import type { DeepKeys, DeepValue, FieldApi, FormApi, FormValidationError, ValidationError, Validator } from "@tanstack/react-form"
|
||||||
|
import { Array, Effect, Either, flow, Layer, ManagedRuntime, pipe } from "effect"
|
||||||
|
import { mapValues } from "remeda"
|
||||||
|
|
||||||
|
|
||||||
|
export const makeSchemaFormValidator = <A, I>(
|
||||||
|
schema: Schema.Schema<A, I, never>,
|
||||||
|
options?: ParseOptions,
|
||||||
|
) => {
|
||||||
|
const decodeEither = Schema.decodeEither(schema, options)
|
||||||
|
|
||||||
|
return <
|
||||||
|
TFormData extends I,
|
||||||
|
TFormValidator extends Validator<TFormData, unknown> | undefined,
|
||||||
|
>(props: {
|
||||||
|
value: TFormData
|
||||||
|
formApi: FormApi<TFormData, TFormValidator>
|
||||||
|
}): FormValidationError<TFormData> =>
|
||||||
|
decodeEither(props.value).pipe(result =>
|
||||||
|
Either.isLeft(result)
|
||||||
|
? {
|
||||||
|
form: "Please check the fields",
|
||||||
|
fields: issuesToFieldsRecord(ArrayFormatter.formatErrorSync(result.left), props.formApi) as any,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSchemaAsyncFormValidator = <A, I>(
|
||||||
|
schema: Schema.Schema<A, I, never>,
|
||||||
|
options?: ParseOptions,
|
||||||
|
) => {
|
||||||
|
const runtime = ManagedRuntime.make(Layer.empty)
|
||||||
|
const decode = Schema.decode(schema, options)
|
||||||
|
|
||||||
|
return <
|
||||||
|
TFormData extends I,
|
||||||
|
TFormValidator extends Validator<TFormData, unknown> | undefined,
|
||||||
|
>(props: {
|
||||||
|
value: TFormData
|
||||||
|
formApi: FormApi<TFormData, TFormValidator>
|
||||||
|
signal: AbortSignal
|
||||||
|
}): Promise<FormValidationError<TFormData>> =>
|
||||||
|
decode(props.value).pipe(
|
||||||
|
Effect.matchEffect({
|
||||||
|
onSuccess: () => Effect.succeed(null),
|
||||||
|
|
||||||
|
onFailure: e => ArrayFormatter.formatError(e).pipe(
|
||||||
|
Effect.map(issues => ({
|
||||||
|
form: "Please check the fields",
|
||||||
|
fields: issuesToFieldsRecord(issues, props.formApi) as any,
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
|
prgm => runtime.runPromise(prgm, { signal: props.signal }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: handle nested array fields
|
||||||
|
*/
|
||||||
|
const issuesToFieldsRecord = <
|
||||||
|
TFormData,
|
||||||
|
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
|
||||||
|
>(
|
||||||
|
issues: readonly ArrayFormatter.Issue[],
|
||||||
|
_formApi: FormApi<TFormData, TFormValidator>,
|
||||||
|
) => pipe(issues,
|
||||||
|
Array.groupBy(issue => issue.path.join(".")),
|
||||||
|
|
||||||
|
mapValues(flow(
|
||||||
|
Array.map(issue => issue.message),
|
||||||
|
Array.join("\n"),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const makeSchemaFieldValidator = <A, I>(
|
||||||
|
schema: Schema.Schema<A, I, never>,
|
||||||
|
options?: ParseOptions,
|
||||||
|
) => {
|
||||||
|
const decodeEither = Schema.decodeEither(schema, options)
|
||||||
|
|
||||||
|
return <
|
||||||
|
TParentData,
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
TFieldValidator extends Validator<DeepValue<TParentData, TName>, unknown> | undefined,
|
||||||
|
TFormValidator extends Validator<TParentData, unknown> | undefined,
|
||||||
|
TData extends I & DeepValue<TParentData, TName>,
|
||||||
|
>(props: {
|
||||||
|
value: TData
|
||||||
|
fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>
|
||||||
|
}): ValidationError =>
|
||||||
|
decodeEither(props.value).pipe(result =>
|
||||||
|
Either.isLeft(result)
|
||||||
|
? ArrayFormatter.formatErrorSync(result.left)
|
||||||
|
.map(issue => issue.message)
|
||||||
|
.join("\n")
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSchemaAsyncFieldValidator = <A, I>(
|
||||||
|
schema: Schema.Schema<A, I, never>,
|
||||||
|
options?: ParseOptions,
|
||||||
|
) => {
|
||||||
|
const runtime = ManagedRuntime.make(Layer.empty)
|
||||||
|
const decode = Schema.decode(schema, options)
|
||||||
|
|
||||||
|
return <
|
||||||
|
TParentData,
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
TFieldValidator extends Validator<DeepValue<TParentData, TName>, unknown> | undefined,
|
||||||
|
TFormValidator extends Validator<TParentData, unknown> | undefined,
|
||||||
|
TData extends I & DeepValue<TParentData, TName>,
|
||||||
|
>(props: {
|
||||||
|
value: TData
|
||||||
|
fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>
|
||||||
|
signal: AbortSignal
|
||||||
|
}): Promise<ValidationError> =>
|
||||||
|
decode(props.value).pipe(
|
||||||
|
Effect.matchEffect({
|
||||||
|
onSuccess: () => Effect.succeed(null),
|
||||||
|
|
||||||
|
onFailure: e => ArrayFormatter.formatError(e).pipe(
|
||||||
|
Effect.map(flow(
|
||||||
|
Array.map(issue => issue.message),
|
||||||
|
Array.join("\n"),
|
||||||
|
))
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
|
prgm => runtime.runPromise(prgm, { signal: props.signal }),
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user