-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver-action-form.ts
More file actions
125 lines (118 loc) · 3.83 KB
/
server-action-form.ts
File metadata and controls
125 lines (118 loc) · 3.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { Cause, Effect, Exit, Layer, ParseResult, Schema as S } from "effect";
import type { HandlerConfig } from "./server-action.ts";
import { Next, NextPayloadError, NextUnexpectedError } from "./next-service.ts";
import { RequestContext } from "./request-context.ts";
export const validateFormData = <FormFields extends S.Schema.AnyNoContext>(
schema: FormFields,
formData: FormData,
): Effect.Effect<S.Schema.Type<FormFields>, DeriveFormErrors<FormFields>> =>
S.decodeUnknown(schema, { errors: "all" })(
Object.fromEntries(formData.entries()),
).pipe(
Effect.mapError((e) => {
const issues = ParseResult.ArrayFormatter.formatErrorSync(e);
const errors: Record<string, string> = {};
for (const issue of issues) {
errors[String(issue.path[0])] = issue.message;
}
return errors as DeriveFormErrors<FormFields>;
}),
);
export type DeriveFormErrors<FormFields extends S.Schema.AnyNoContext> = {
[K in keyof S.Schema.Type<FormFields>]: string;
};
export type FormState<
State extends S.Schema.AnyNoContext,
FormFields extends S.Schema.AnyNoContext,
> = S.Schema.Type<State> & { errors?: DeriveFormErrors<FormFields> };
export type FormHandlerConfig<
State extends S.Schema.AnyNoContext,
FormFields extends S.Schema.AnyNoContext,
InternalServerError,
InvalidPayloadError,
ProvidedServices,
> =
& {
state: State;
fields: FormFields;
action: (
prevState: FormState<State, FormFields>,
formFields: S.Schema.Type<FormFields>,
) => Promise<
Effect.Effect<
FormState<State, FormFields>,
FormState<State, FormFields> | NextUnexpectedError,
ProvidedServices | Next
>
>;
errors: {
invalidFormData: (
errors: DeriveFormErrors<FormFields>,
schema: FormFields,
rawPayload: FormData,
) => FormState<State, FormFields>;
unexpected: (cause: Cause.Cause<unknown>) => FormState<State, FormFields>;
};
}
& Omit<
HandlerConfig<InternalServerError, ProvidedServices, InvalidPayloadError>,
"errors"
>;
export const makeFormHandler = <
State extends S.Schema.AnyNoContext,
FormFields extends S.Schema.AnyNoContext,
InternalServerError,
InvalidPayloadError,
ProvidedServices,
>(
config: FormHandlerConfig<
State,
FormFields,
InternalServerError,
InvalidPayloadError,
ProvidedServices
>,
) => {
const mergedContext = Layer.mergeAll(
config.layer ?? Layer.empty,
Next.Default,
);
return async (
prevState: FormState<State, FormFields>,
formData: FormData,
): Promise<FormState<State, FormFields>> => {
const requestContext = RequestContext.of({
rawRequest: formData,
type: "server-action",
requestId: config.generateRequestId?.() ?? crypto.randomUUID(),
});
const effect = Effect.gen(function* () {
const formFields = yield* validateFormData(config.fields, formData).pipe(
Effect.mapError((errors) =>
config.errors.invalidFormData(errors, config.fields, formData)
),
);
const effectFn = yield* Effect.promise(() =>
config.action(prevState, formFields)
);
return yield* effectFn.pipe(
Effect.catchTag(
"NextUnexpectedError",
(error) => Effect.fail(config.errors.unexpected(Cause.fail(error))),
),
);
}).pipe(
Effect.provide(mergedContext),
Effect.provideService(RequestContext, requestContext),
);
// @ts-expect-error: typescript fails to infer but its right
const programExit = await Effect.runPromiseExit(effect);
if (Exit.isSuccess(programExit)) {
return programExit.value as FormState<State, FormFields>;
}
if (Cause.isFailType(programExit.cause)) {
return programExit.cause.error as FormState<State, FormFields>;
}
return config.errors.unexpected(programExit.cause);
};
};