@formadapter/orpc 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +139 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ludicrous
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# `@formadapter/orpc`
|
|
2
|
+
|
|
3
|
+
Connect a FormAdapter form to an oRPC procedure without coupling the form to a
|
|
4
|
+
router, transport, or UI adapter.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
bun add @formadapter/orpc @orpc/client
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Client procedures
|
|
11
|
+
|
|
12
|
+
`createORPCSubmission` returns a FormAdapter `onSubmit` handler. It sends the
|
|
13
|
+
schema input from `SubmitContext` by default, not the transformed output passed
|
|
14
|
+
as the first argument. That lets the server run the schema's transforms again
|
|
15
|
+
at its trust boundary. The form's abort signal is forwarded through oRPC caller
|
|
16
|
+
options.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { createORPCSubmission } from "@formadapter/orpc";
|
|
20
|
+
|
|
21
|
+
<Account.Form onSubmit={createORPCSubmission(orpc.account.update)} />;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The procedure output is wrapped as `{ status: "success", data }`. A procedure
|
|
25
|
+
that already returns a well-formed FormAdapter `SubmissionState` passes through
|
|
26
|
+
unchanged.
|
|
27
|
+
|
|
28
|
+
Use `mapInput` only when the form and procedure intentionally have different
|
|
29
|
+
wire shapes. Caller options and oRPC context can be static values or per-submit
|
|
30
|
+
factories:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const submit = createORPCSubmission(orpc.account.update, {
|
|
34
|
+
mapInput: (values) => ({ account: values }),
|
|
35
|
+
context: (_values, context) => ({ requestId: context.formData.get("requestId") }),
|
|
36
|
+
options: () => ({ lastEventId: currentEventId }),
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Unknown thrown values and server-side failures receive a safe generic message.
|
|
41
|
+
Use `mapError` for application-specific errors; returning `undefined` falls
|
|
42
|
+
through to the built-in mappings.
|
|
43
|
+
|
|
44
|
+
## Typed form errors
|
|
45
|
+
|
|
46
|
+
The package has no `@orpc/server` dependency. It exports a structural Standard
|
|
47
|
+
Schema error declaration that can be installed once on an oRPC base builder:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { FORM_SUBMISSION_ERROR_MAP } from "@formadapter/orpc";
|
|
51
|
+
import { os } from "@orpc/server";
|
|
52
|
+
|
|
53
|
+
const formProcedure = os.errors(FORM_SUBMISSION_ERROR_MAP);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
In a handler, return the error through oRPC's generated constructor:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
throw errors.FORM_SUBMISSION_FAILED({
|
|
60
|
+
data: submissionFailure({
|
|
61
|
+
fieldErrors: { email: ["That address is already registered."] },
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`createORPCSubmission` maps that data directly into form state. It also maps
|
|
67
|
+
oRPC's built-in `BAD_REQUEST` `{ issues }` payload, preserving nested and array
|
|
68
|
+
Standard Schema paths.
|
|
69
|
+
|
|
70
|
+
That `BAD_REQUEST` mapping is runtime fallback behavior, not a declared custom
|
|
71
|
+
error. It works with oRPC's default input-validation payload. If an interceptor
|
|
72
|
+
replaces or removes `data.issues`, use a typed custom error or `mapError` instead.
|
|
73
|
+
|
|
74
|
+
## Actionable procedures
|
|
75
|
+
|
|
76
|
+
Use `createORPCActionSubmission` for the tuple returned by `.actionable()`.
|
|
77
|
+
This is separate because actionable procedures return JSON errors instead of
|
|
78
|
+
throwing ordinary `ORPCError` instances and cannot accept an AbortSignal.
|
|
79
|
+
Unexpected throws are deliberately re-thrown so framework redirects and
|
|
80
|
+
not-found control flow continue to work.
|
|
81
|
+
|
|
82
|
+
`@orpc/react`'s `createFormAction` is a different integration: it consumes
|
|
83
|
+
bracket-notation `FormData` and returns no result state. FormAdapter uses its
|
|
84
|
+
typed object submission path here so errors can be applied to the form.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { FailedSubmission, MaybePromise, StandardResult, SubmissionState, SuccessfulSubmission } from "@formadapter/core";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/** The structural callable shape shared by oRPC procedure clients. */
|
|
5
|
+
type ORPCCallable = (...args: never[]) => unknown;
|
|
6
|
+
type ClientArguments<Client extends ORPCCallable> = Client extends ((...args: infer Arguments) => unknown) ? Arguments : never;
|
|
7
|
+
type SecondArgument<Arguments extends readonly unknown[]> = Arguments extends readonly [unknown, ...infer Rest] ? Rest[0] : never;
|
|
8
|
+
type ClientResult<Client extends ORPCCallable> = Client extends ((...args: never[]) => infer Result) ? Result : never;
|
|
9
|
+
type NonNever<Value, Fallback> = [Value] extends [never] ? Fallback : Value;
|
|
10
|
+
/** The input accepted by an oRPC procedure client. */
|
|
11
|
+
type ORPCClientInput<Client extends ORPCCallable> = ClientArguments<Client>[0];
|
|
12
|
+
/** The resolved output returned by an oRPC procedure client. */
|
|
13
|
+
type ORPCClientOutput<Client extends ORPCCallable> = Awaited<ClientResult<Client>>;
|
|
14
|
+
/** The error recorded on oRPC's structurally typed PromiseWithError. */
|
|
15
|
+
type ORPCClientError<Client extends ORPCCallable> = ClientResult<Client> extends {
|
|
16
|
+
readonly __error?: {
|
|
17
|
+
readonly type: infer ErrorType;
|
|
18
|
+
};
|
|
19
|
+
} ? ErrorType : unknown;
|
|
20
|
+
type InferredCallerOptions<Client extends ORPCCallable> = NonNullable<SecondArgument<ClientArguments<Client>>>;
|
|
21
|
+
/** Caller options accepted by the client, excluding values controlled by this adapter. */
|
|
22
|
+
type ORPCCallerOptions<Client extends ORPCCallable> = NonNever<InferredCallerOptions<Client> extends object ? Omit<InferredCallerOptions<Client>, "context" | "signal"> : never, Readonly<Record<never, never>>>;
|
|
23
|
+
/** The per-call oRPC client context, if the client defines one. */
|
|
24
|
+
type ORPCClientContext<Client extends ORPCCallable> = NonNever<InferredCallerOptions<Client> extends {
|
|
25
|
+
readonly context?: infer Context;
|
|
26
|
+
} ? Context : never, Readonly<Record<never, never>>>;
|
|
27
|
+
type ClientRequiresContext<Client extends ORPCCallable> = [InferredCallerOptions<Client>] extends [never] ? false : InferredCallerOptions<Client> extends {
|
|
28
|
+
readonly context: unknown;
|
|
29
|
+
} ? true : false;
|
|
30
|
+
/** The structural FormAdapter context supplied to an oRPC submission handler. */
|
|
31
|
+
interface ORPCSubmitContext<Input> {
|
|
32
|
+
/** Prepared schema input before transforms. */
|
|
33
|
+
readonly input: Input;
|
|
34
|
+
readonly formData: FormData;
|
|
35
|
+
readonly signal: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
type DataFromSubmissionState<State> = State extends SuccessfulSubmission<infer Data> ? Data : never;
|
|
38
|
+
/** Unwraps procedures that already return a shared SubmissionState. */
|
|
39
|
+
type ORPCSubmissionData<Output> = [Output] extends [SubmissionState] ? DataFromSubmissionState<Output> : Output;
|
|
40
|
+
type ORPCSubmissionHandler<Input, Values, Data> = (values: Values, context: ORPCSubmitContext<Input>) => Promise<SubmissionState<Data>>;
|
|
41
|
+
type ORPCValueOrFactory<Value, Values, Input> = Value | ((values: Values, context: ORPCSubmitContext<Input>) => MaybePromise<Value>);
|
|
42
|
+
interface ORPCErrorLike<Data = unknown> {
|
|
43
|
+
readonly code: string;
|
|
44
|
+
readonly data?: Data;
|
|
45
|
+
readonly defined?: boolean;
|
|
46
|
+
readonly message: string;
|
|
47
|
+
readonly status: number;
|
|
48
|
+
}
|
|
49
|
+
type ORPCErrorMapper<ErrorType, Values, Input, Data> = (error: ErrorType, values: Values, context: ORPCSubmitContext<Input>) => MaybePromise<SubmissionState<Data> | undefined>;
|
|
50
|
+
interface ORPCSubmissionOptionsBase<Client extends ORPCCallable, Input, Values> {
|
|
51
|
+
/** Maps form values/context to the procedure input. Defaults to context.input. */
|
|
52
|
+
readonly mapInput?: (values: Values, context: ORPCSubmitContext<Input>) => MaybePromise<ORPCClientInput<Client>>;
|
|
53
|
+
/** Handles application-specific oRPC errors before the built-in mappings run. */
|
|
54
|
+
readonly mapError?: ORPCErrorMapper<ORPCClientError<Client>, Values, Input, ORPCSubmissionData<ORPCClientOutput<Client>>>;
|
|
55
|
+
/** Static caller options or a per-submit factory. Signal and context are managed separately. */
|
|
56
|
+
readonly options?: ORPCValueOrFactory<ORPCCallerOptions<Client>, Values, Input>;
|
|
57
|
+
/** Safe message used for unknown, malformed, and server-side failures. */
|
|
58
|
+
readonly unknownErrorMessage?: string;
|
|
59
|
+
}
|
|
60
|
+
type ORPCContextOption<Client extends ORPCCallable, Input, Values> = ClientRequiresContext<Client> extends true ? {
|
|
61
|
+
readonly context: ORPCValueOrFactory<ORPCClientContext<Client>, Values, Input>;
|
|
62
|
+
} : {
|
|
63
|
+
readonly context?: ORPCValueOrFactory<ORPCClientContext<Client>, Values, Input>;
|
|
64
|
+
};
|
|
65
|
+
type ORPCSubmissionOptions<Client extends ORPCCallable, Input = ORPCClientInput<Client>, Values = unknown> = ORPCSubmissionOptionsBase<Client, Input, Values> & ORPCContextOption<Client, Input, Values>;
|
|
66
|
+
type ORPCSubmissionOptionsRest<Client extends ORPCCallable, Input, Values> = ClientRequiresContext<Client> extends true ? [options: ORPCSubmissionOptions<Client, Input, Values>] : [options?: ORPCSubmissionOptions<Client, Input, Values>];
|
|
67
|
+
/** JSON-safe error returned by an oRPC actionable procedure. */
|
|
68
|
+
interface ORPCActionError<Data = unknown> extends ORPCErrorLike<Data> {
|
|
69
|
+
readonly defined: boolean;
|
|
70
|
+
}
|
|
71
|
+
type ORPCActionResult<Output, ErrorType extends ORPCErrorLike> = readonly [error: null, data: Output] | readonly [error: ErrorType, data: undefined];
|
|
72
|
+
type ORPCAction<Input, Output, ErrorType extends ORPCErrorLike> = (input: Input) => PromiseLike<ORPCActionResult<Output, ErrorType>>;
|
|
73
|
+
interface ORPCActionSubmissionOptions<ActionInput, Output, ErrorType extends ORPCErrorLike, Input = ActionInput, Values = unknown> {
|
|
74
|
+
readonly mapInput?: (values: Values, context: ORPCSubmitContext<Input>) => MaybePromise<ActionInput>;
|
|
75
|
+
readonly mapError?: ORPCErrorMapper<ErrorType, Values, Input, ORPCSubmissionData<Output>>;
|
|
76
|
+
readonly unknownErrorMessage?: string;
|
|
77
|
+
}
|
|
78
|
+
/** Minimal Standard Schema shape used by oRPC error data declarations. */
|
|
79
|
+
interface ORPCStandardSchema<Input, Output = Input> {
|
|
80
|
+
readonly "~standard": {
|
|
81
|
+
readonly version: 1;
|
|
82
|
+
readonly vendor: string;
|
|
83
|
+
readonly types?: {
|
|
84
|
+
readonly input: Input;
|
|
85
|
+
readonly output: Output;
|
|
86
|
+
};
|
|
87
|
+
readonly validate: (value: unknown) => StandardResult<Output>;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Kept as an object type alias (rather than an interface) so it is assignable
|
|
92
|
+
* to oRPC 1.x's open-ended `ErrorMap` without widening the known error code.
|
|
93
|
+
*/
|
|
94
|
+
type FormSubmissionErrorMap = {
|
|
95
|
+
readonly FORM_SUBMISSION_FAILED: {
|
|
96
|
+
readonly data: ORPCStandardSchema<FailedSubmission>;
|
|
97
|
+
readonly message: "Form submission failed";
|
|
98
|
+
readonly status: 422;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/error-data.d.ts
|
|
103
|
+
declare const FORM_SUBMISSION_FAILED_CODE: "FORM_SUBMISSION_FAILED";
|
|
104
|
+
/** Runtime guard for error data sent through FORM_SUBMISSION_FAILED. */
|
|
105
|
+
declare function isFormSubmissionFailureData(value: unknown): value is FailedSubmission;
|
|
106
|
+
/** Standard Schema used for a typed oRPC FORM_SUBMISSION_FAILED error. */
|
|
107
|
+
declare const formSubmissionFailureSchema: ORPCStandardSchema<FailedSubmission>;
|
|
108
|
+
/** Reusable argument for `os.errors(FORM_SUBMISSION_ERROR_MAP)`. */
|
|
109
|
+
declare const FORM_SUBMISSION_ERROR_MAP: FormSubmissionErrorMap;
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/errors.d.ts
|
|
112
|
+
declare const DEFAULT_ORPC_ERROR_MESSAGE = "Unable to submit the form. Please try again.";
|
|
113
|
+
declare function isORPCErrorLike(value: unknown): value is ORPCErrorLike;
|
|
114
|
+
declare function orpcErrorToSubmission(error: unknown, unknownErrorMessage?: string): SubmissionState | undefined;
|
|
115
|
+
declare function unknownErrorToSubmission(error: unknown, unknownErrorMessage?: string): SubmissionState;
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/submission.d.ts
|
|
118
|
+
/** Adapts a normal oRPC procedure client to a FormAdapter onSubmit handler. */
|
|
119
|
+
declare function createORPCSubmission<Client extends ORPCCallable, Input = ORPCClientInput<Client>, Values = unknown>(client: Client, ...[options]: ORPCSubmissionOptionsRest<Client, Input, Values>): ORPCSubmissionHandler<Input, Values, ORPCSubmissionData<ORPCClientOutput<Client>>>;
|
|
120
|
+
/**
|
|
121
|
+
* Adapts an oRPC `.actionable()` tuple to FormAdapter.
|
|
122
|
+
* Unexpected throws are intentionally preserved for framework redirects/not-found control flow.
|
|
123
|
+
*/
|
|
124
|
+
declare function createORPCActionSubmission<ActionInput, Output, ErrorType extends ORPCErrorLike, Input = ActionInput, Values = unknown>(action: ORPCAction<ActionInput, Output, ErrorType>, options?: ORPCActionSubmissionOptions<ActionInput, Output, ErrorType, Input, Values>): ORPCSubmissionHandler<Input, Values, ORPCSubmissionData<Output>>;
|
|
125
|
+
//#endregion
|
|
126
|
+
export { DEFAULT_ORPC_ERROR_MESSAGE, FORM_SUBMISSION_ERROR_MAP, FORM_SUBMISSION_FAILED_CODE, type FormSubmissionErrorMap, type ORPCAction, type ORPCActionError, type ORPCActionResult, type ORPCActionSubmissionOptions, type ORPCCallable, type ORPCCallerOptions, type ORPCClientContext, type ORPCClientError, type ORPCClientInput, type ORPCClientOutput, type ORPCErrorLike, type ORPCErrorMapper, type ORPCStandardSchema, type ORPCSubmissionData, type ORPCSubmissionHandler, type ORPCSubmissionOptions, type ORPCSubmitContext, type ORPCValueOrFactory, createORPCActionSubmission, createORPCSubmission, formSubmissionFailureSchema, isFormSubmissionFailureData, isORPCErrorLike, orpcErrorToSubmission, unknownErrorToSubmission };
|
|
127
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { isSubmissionState, issuePath, issuesToFieldErrors, submissionFailure, submissionSuccess } from "@formadapter/core";
|
|
2
|
+
//#region src/error-data.ts
|
|
3
|
+
const FORM_SUBMISSION_FAILED_CODE = "FORM_SUBMISSION_FAILED";
|
|
4
|
+
/** Runtime guard for error data sent through FORM_SUBMISSION_FAILED. */
|
|
5
|
+
function isFormSubmissionFailureData(value) {
|
|
6
|
+
try {
|
|
7
|
+
return isSubmissionState(value) && value.status === "error";
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function validateFailureData(value) {
|
|
13
|
+
return isFormSubmissionFailureData(value) ? { value } : { issues: [{ message: "Expected a FormAdapter failed submission." }] };
|
|
14
|
+
}
|
|
15
|
+
/** Standard Schema used for a typed oRPC FORM_SUBMISSION_FAILED error. */
|
|
16
|
+
const formSubmissionFailureSchema = { "~standard": {
|
|
17
|
+
validate: validateFailureData,
|
|
18
|
+
vendor: "formadapter",
|
|
19
|
+
version: 1
|
|
20
|
+
} };
|
|
21
|
+
/** Reusable argument for `os.errors(FORM_SUBMISSION_ERROR_MAP)`. */
|
|
22
|
+
const FORM_SUBMISSION_ERROR_MAP = { FORM_SUBMISSION_FAILED: {
|
|
23
|
+
data: formSubmissionFailureSchema,
|
|
24
|
+
message: "Form submission failed",
|
|
25
|
+
status: 422
|
|
26
|
+
} };
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/errors.ts
|
|
29
|
+
const DEFAULT_ORPC_ERROR_MESSAGE = "Unable to submit the form. Please try again.";
|
|
30
|
+
function isRecord(value) {
|
|
31
|
+
return typeof value === "object" && value !== null;
|
|
32
|
+
}
|
|
33
|
+
function isPathSegment(value) {
|
|
34
|
+
try {
|
|
35
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "symbol") return true;
|
|
36
|
+
return isRecord(value) && "key" in value && (typeof value.key === "string" || typeof value.key === "number" || typeof value.key === "symbol");
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isStandardIssue(value) {
|
|
42
|
+
try {
|
|
43
|
+
if (!isRecord(value) || typeof value.message !== "string") return false;
|
|
44
|
+
return value.path === void 0 || Array.isArray(value.path) && value.path.every(isPathSegment);
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function isORPCErrorLike(value) {
|
|
50
|
+
try {
|
|
51
|
+
return isRecord(value) && typeof value.code === "string" && typeof value.message === "string" && typeof value.status === "number" && Number.isFinite(value.status);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function validationFailure(issues, fallbackMessage) {
|
|
57
|
+
const fieldIssues = issues.filter((issue) => issuePath(issue).length > 0);
|
|
58
|
+
const formErrors = issues.filter((issue) => issuePath(issue).length === 0).map((issue) => issue.message);
|
|
59
|
+
return submissionFailure({
|
|
60
|
+
errorKind: "validation",
|
|
61
|
+
fieldErrors: issuesToFieldErrors(fieldIssues),
|
|
62
|
+
formErrors: fieldIssues.length === 0 && formErrors.length === 0 ? [fallbackMessage] : formErrors
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function issuesFromError(error) {
|
|
66
|
+
if (!isRecord(error.data) || !Array.isArray(error.data.issues)) return;
|
|
67
|
+
return error.data.issues.filter(isStandardIssue);
|
|
68
|
+
}
|
|
69
|
+
function orpcErrorToSubmission(error, unknownErrorMessage = DEFAULT_ORPC_ERROR_MESSAGE) {
|
|
70
|
+
try {
|
|
71
|
+
if (!isORPCErrorLike(error)) return void 0;
|
|
72
|
+
if (error.code === "FORM_SUBMISSION_FAILED" && isFormSubmissionFailureData(error.data)) return error.data;
|
|
73
|
+
const issues = error.code === "BAD_REQUEST" ? issuesFromError(error) : void 0;
|
|
74
|
+
if (issues) return validationFailure(issues, error.message || unknownErrorMessage);
|
|
75
|
+
if (error.status >= 400 && error.status < 500 && error.message.trim().length > 0) return submissionFailure({
|
|
76
|
+
errorKind: "business",
|
|
77
|
+
formErrors: [error.message]
|
|
78
|
+
});
|
|
79
|
+
return submissionFailure({
|
|
80
|
+
errorKind: "transport",
|
|
81
|
+
formErrors: [unknownErrorMessage]
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function unknownErrorToSubmission(error, unknownErrorMessage = DEFAULT_ORPC_ERROR_MESSAGE) {
|
|
88
|
+
return orpcErrorToSubmission(error, unknownErrorMessage) ?? submissionFailure({
|
|
89
|
+
errorKind: "transport",
|
|
90
|
+
formErrors: [unknownErrorMessage]
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/submission.ts
|
|
95
|
+
async function resolveValue(value, values, context) {
|
|
96
|
+
if (typeof value === "function") return await value(values, context);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function normalizeOutput(output) {
|
|
100
|
+
return isSubmissionState(output) ? output : submissionSuccess(output);
|
|
101
|
+
}
|
|
102
|
+
function safeCallerOptions(value) {
|
|
103
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
104
|
+
const result = { ...value };
|
|
105
|
+
delete result.context;
|
|
106
|
+
delete result.signal;
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/** Adapts a normal oRPC procedure client to a FormAdapter onSubmit handler. */
|
|
110
|
+
function createORPCSubmission(client, ...[options]) {
|
|
111
|
+
return async (values, context) => {
|
|
112
|
+
const input = options?.mapInput ? await options.mapInput(values, context) : context.input;
|
|
113
|
+
const suppliedOptions = await resolveValue(options?.options, values, context);
|
|
114
|
+
const suppliedContext = await resolveValue(options?.context, values, context);
|
|
115
|
+
const callerOptions = safeCallerOptions(suppliedOptions);
|
|
116
|
+
if (suppliedContext !== void 0) callerOptions.context = suppliedContext;
|
|
117
|
+
callerOptions.signal = context.signal;
|
|
118
|
+
try {
|
|
119
|
+
return normalizeOutput(await client(input, callerOptions));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return (options?.mapError ? await options.mapError(error, values, context) : void 0) ?? unknownErrorToSubmission(error, options?.unknownErrorMessage);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Adapts an oRPC `.actionable()` tuple to FormAdapter.
|
|
127
|
+
* Unexpected throws are intentionally preserved for framework redirects/not-found control flow.
|
|
128
|
+
*/
|
|
129
|
+
function createORPCActionSubmission(action, options = {}) {
|
|
130
|
+
return async (values, context) => {
|
|
131
|
+
const [error, output] = await action(options.mapInput ? await options.mapInput(values, context) : context.input);
|
|
132
|
+
if (error !== null) return (options.mapError ? await options.mapError(error, values, context) : void 0) ?? unknownErrorToSubmission(error, options.unknownErrorMessage);
|
|
133
|
+
return normalizeOutput(output);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { DEFAULT_ORPC_ERROR_MESSAGE, FORM_SUBMISSION_ERROR_MAP, FORM_SUBMISSION_FAILED_CODE, createORPCActionSubmission, createORPCSubmission, formSubmissionFailureSchema, isFormSubmissionFailureData, isORPCErrorLike, orpcErrorToSubmission, unknownErrorToSubmission };
|
|
138
|
+
|
|
139
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["call"],"sources":["../src/error-data.ts","../src/errors.ts","../src/submission.ts"],"sourcesContent":["import { isSubmissionState } from \"@formadapter/core\";\nimport type { FailedSubmission, StandardResult } from \"@formadapter/core\";\n\nimport type {\n FormSubmissionErrorMap,\n ORPCStandardSchema,\n} from \"./types\";\n\nexport const FORM_SUBMISSION_FAILED_CODE = \"FORM_SUBMISSION_FAILED\" as const;\n\n/** Runtime guard for error data sent through FORM_SUBMISSION_FAILED. */\nexport function isFormSubmissionFailureData(\n value: unknown,\n): value is FailedSubmission {\n try {\n return isSubmissionState(value) && value.status === \"error\";\n } catch {\n return false;\n }\n}\n\nfunction validateFailureData(value: unknown): StandardResult<FailedSubmission> {\n return isFormSubmissionFailureData(value)\n ? { value }\n : {\n issues: [\n {\n message: \"Expected a FormAdapter failed submission.\",\n },\n ],\n };\n}\n\n/** Standard Schema used for a typed oRPC FORM_SUBMISSION_FAILED error. */\nexport const formSubmissionFailureSchema: ORPCStandardSchema<FailedSubmission> = {\n \"~standard\": {\n validate: validateFailureData,\n vendor: \"formadapter\",\n version: 1,\n },\n};\n\n/** Reusable argument for `os.errors(FORM_SUBMISSION_ERROR_MAP)`. */\nexport const FORM_SUBMISSION_ERROR_MAP: FormSubmissionErrorMap = {\n FORM_SUBMISSION_FAILED: {\n data: formSubmissionFailureSchema,\n message: \"Form submission failed\",\n status: 422,\n },\n};\n","import {\n issuePath,\n issuesToFieldErrors,\n submissionFailure,\n} from \"@formadapter/core\";\nimport type {\n StandardIssue,\n SubmissionState,\n} from \"@formadapter/core\";\n\nimport {\n FORM_SUBMISSION_FAILED_CODE,\n isFormSubmissionFailureData,\n} from \"./error-data\";\nimport type { ORPCErrorLike } from \"./types\";\n\nexport const DEFAULT_ORPC_ERROR_MESSAGE =\n \"Unable to submit the form. Please try again.\";\n\nfunction isRecord(value: unknown): value is Readonly<Record<string, unknown>> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isPathSegment(value: unknown): boolean {\n try {\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"symbol\"\n ) {\n return true;\n }\n return (\n isRecord(value) &&\n \"key\" in value &&\n (typeof value.key === \"string\" ||\n typeof value.key === \"number\" ||\n typeof value.key === \"symbol\")\n );\n } catch {\n return false;\n }\n}\n\nfunction isStandardIssue(value: unknown): value is StandardIssue {\n try {\n if (!isRecord(value) || typeof value.message !== \"string\") return false;\n return (\n value.path === undefined ||\n (Array.isArray(value.path) && value.path.every(isPathSegment))\n );\n } catch {\n return false;\n }\n}\n\nexport function isORPCErrorLike(value: unknown): value is ORPCErrorLike {\n try {\n return (\n isRecord(value) &&\n typeof value.code === \"string\" &&\n typeof value.message === \"string\" &&\n typeof value.status === \"number\" &&\n Number.isFinite(value.status)\n );\n } catch {\n return false;\n }\n}\n\nfunction validationFailure(\n issues: readonly StandardIssue[],\n fallbackMessage: string,\n): SubmissionState {\n const fieldIssues = issues.filter((issue) => issuePath(issue).length > 0);\n const formErrors = issues\n .filter((issue) => issuePath(issue).length === 0)\n .map((issue) => issue.message);\n\n return submissionFailure({\n errorKind: \"validation\",\n fieldErrors: issuesToFieldErrors(fieldIssues),\n formErrors:\n fieldIssues.length === 0 && formErrors.length === 0\n ? [fallbackMessage]\n : formErrors,\n });\n}\n\nfunction issuesFromError(error: ORPCErrorLike): readonly StandardIssue[] | undefined {\n if (!isRecord(error.data) || !Array.isArray(error.data.issues)) {\n return undefined;\n }\n return error.data.issues.filter(isStandardIssue);\n}\n\nexport function orpcErrorToSubmission(\n error: unknown,\n unknownErrorMessage: string = DEFAULT_ORPC_ERROR_MESSAGE,\n): SubmissionState | undefined {\n try {\n if (!isORPCErrorLike(error)) return undefined;\n\n if (\n error.code === FORM_SUBMISSION_FAILED_CODE &&\n isFormSubmissionFailureData(error.data)\n ) {\n return error.data;\n }\n\n const issues = error.code === \"BAD_REQUEST\"\n ? issuesFromError(error)\n : undefined;\n if (issues) {\n return validationFailure(issues, error.message || unknownErrorMessage);\n }\n\n if (\n error.status >= 400 &&\n error.status < 500 &&\n error.message.trim().length > 0\n ) {\n return submissionFailure({\n errorKind: \"business\",\n formErrors: [error.message],\n });\n }\n\n return submissionFailure({\n errorKind: \"transport\",\n formErrors: [unknownErrorMessage],\n });\n } catch {\n return undefined;\n }\n}\n\nexport function unknownErrorToSubmission(\n error: unknown,\n unknownErrorMessage: string = DEFAULT_ORPC_ERROR_MESSAGE,\n): SubmissionState {\n return (\n orpcErrorToSubmission(error, unknownErrorMessage) ??\n submissionFailure({\n errorKind: \"transport\",\n formErrors: [unknownErrorMessage],\n })\n );\n}\n","import {\n isSubmissionState,\n submissionSuccess,\n} from \"@formadapter/core\";\nimport type {\n SubmissionState,\n} from \"@formadapter/core\";\n\nimport { unknownErrorToSubmission } from \"./errors\";\nimport type {\n ORPCAction,\n ORPCActionSubmissionOptions,\n ORPCCallable,\n ORPCClientInput,\n ORPCClientOutput,\n ORPCErrorLike,\n ORPCSubmissionData,\n ORPCSubmissionHandler,\n ORPCSubmissionOptionsRest,\n ORPCSubmitContext,\n ORPCValueOrFactory,\n} from \"./types\";\n\nasync function resolveValue<Value, Values, Input>(\n value: ORPCValueOrFactory<Value, Values, Input> | undefined,\n values: Values,\n context: ORPCSubmitContext<Input>,\n): Promise<Value | undefined> {\n if (typeof value === \"function\") {\n return await (\n value as (\n currentValues: Values,\n currentContext: typeof context,\n ) => Value | PromiseLike<Value>\n )(values, context);\n }\n return value;\n}\n\nfunction normalizeOutput<Output>(\n output: Output,\n): SubmissionState<ORPCSubmissionData<Output>> {\n return isSubmissionState(output)\n ? (output as SubmissionState<ORPCSubmissionData<Output>>)\n : submissionSuccess(output as ORPCSubmissionData<Output>);\n}\n\nfunction safeCallerOptions(value: unknown): Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return {};\n }\n const result = { ...(value as Readonly<Record<string, unknown>>) };\n delete result.context;\n delete result.signal;\n return result;\n}\n\n/** Adapts a normal oRPC procedure client to a FormAdapter onSubmit handler. */\nexport function createORPCSubmission<\n Client extends ORPCCallable,\n Input = ORPCClientInput<Client>,\n Values = unknown,\n>(\n client: Client,\n ...[options]: ORPCSubmissionOptionsRest<Client, Input, Values>\n): ORPCSubmissionHandler<\n Input,\n Values,\n ORPCSubmissionData<ORPCClientOutput<Client>>\n> {\n return async (values, context) => {\n const input = options?.mapInput\n ? await options.mapInput(values, context)\n : context.input;\n const suppliedOptions = await resolveValue(\n options?.options,\n values,\n context,\n );\n const suppliedContext = await resolveValue(\n options?.context,\n values,\n context,\n );\n const callerOptions = safeCallerOptions(suppliedOptions);\n if (suppliedContext !== undefined) {\n callerOptions.context = suppliedContext;\n }\n callerOptions.signal = context.signal;\n\n try {\n const call = client as unknown as (\n procedureInput: ORPCClientInput<Client>,\n procedureOptions: Readonly<Record<string, unknown>>,\n ) => unknown;\n const output = (await call(\n input as ORPCClientInput<Client>,\n callerOptions,\n )) as ORPCClientOutput<Client>;\n return normalizeOutput(output);\n } catch (error) {\n const custom = options?.mapError\n ? await options.mapError(\n error as Parameters<NonNullable<typeof options.mapError>>[0],\n values,\n context,\n )\n : undefined;\n return (\n custom ??\n (unknownErrorToSubmission(\n error,\n options?.unknownErrorMessage,\n ) as SubmissionState<ORPCSubmissionData<ORPCClientOutput<Client>>>)\n );\n }\n };\n}\n\n/**\n * Adapts an oRPC `.actionable()` tuple to FormAdapter.\n * Unexpected throws are intentionally preserved for framework redirects/not-found control flow.\n */\nexport function createORPCActionSubmission<\n ActionInput,\n Output,\n ErrorType extends ORPCErrorLike,\n Input = ActionInput,\n Values = unknown,\n>(\n action: ORPCAction<ActionInput, Output, ErrorType>,\n options: ORPCActionSubmissionOptions<\n ActionInput,\n Output,\n ErrorType,\n Input,\n Values\n > = {},\n): ORPCSubmissionHandler<Input, Values, ORPCSubmissionData<Output>> {\n return async (values, context) => {\n const input = options.mapInput\n ? await options.mapInput(values, context)\n : context.input;\n const [error, output] = await action(input as ActionInput);\n\n if (error !== null) {\n const custom = options.mapError\n ? await options.mapError(error, values, context)\n : undefined;\n return (\n custom ??\n (unknownErrorToSubmission(\n error,\n options.unknownErrorMessage,\n ) as SubmissionState<ORPCSubmissionData<Output>>)\n );\n }\n\n return normalizeOutput(output as Output);\n };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@formadapter/orpc",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Type-safe oRPC submissions for FormAdapter forms.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"forms",
|
|
7
|
+
"orpc",
|
|
8
|
+
"rpc",
|
|
9
|
+
"standard-schema",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://formadapter.com",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ludicroushq/formadapter/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/ludicroushq/formadapter.git",
|
|
19
|
+
"directory": "packages/orpc"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "ludicrous",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"module": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js",
|
|
40
|
+
"default": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@formadapter/core": "^0.0.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@orpc/client": ">=1.14.0 <2.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@formadapter/react": "^0.0.0",
|
|
52
|
+
"@orpc/client": "1.14.7",
|
|
53
|
+
"@orpc/server": "1.14.7"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsdown",
|
|
57
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo .turbo",
|
|
58
|
+
"dev": "tsdown --watch",
|
|
59
|
+
"test": "vitest run --config vitest.config.ts",
|
|
60
|
+
"test:coverage": "vitest run --config vitest.config.ts --coverage",
|
|
61
|
+
"typecheck": "tsc --project tsconfig.json --noEmit"
|
|
62
|
+
}
|
|
63
|
+
}
|