orpc-fn 0.1.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 +250 -0
- package/dist/bound-call.d.ts +23 -0
- package/dist/bound-call.d.ts.map +1 -0
- package/dist/bound-call.js +23 -0
- package/dist/bound-call.js.map +1 -0
- package/dist/client.d.ts +44 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +49 -0
- package/dist/client.js.map +1 -0
- package/dist/create-fn.d.ts +97 -0
- package/dist/create-fn.d.ts.map +1 -0
- package/dist/create-fn.js +224 -0
- package/dist/create-fn.js.map +1 -0
- package/dist/expected-client-error.d.ts +11 -0
- package/dist/expected-client-error.d.ts.map +1 -0
- package/dist/expected-client-error.js +21 -0
- package/dist/expected-client-error.js.map +1 -0
- package/dist/expo.d.ts +66 -0
- package/dist/expo.d.ts.map +1 -0
- package/dist/expo.js +74 -0
- package/dist/expo.js.map +1 -0
- package/dist/hono.d.ts +81 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +138 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schema.d.ts +47 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +44 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/live/codec.d.ts +9 -0
- package/dist/live/codec.d.ts.map +1 -0
- package/dist/live/codec.js +27 -0
- package/dist/live/codec.js.map +1 -0
- package/dist/live/fn-live.d.ts +155 -0
- package/dist/live/fn-live.d.ts.map +1 -0
- package/dist/live/fn-live.js +210 -0
- package/dist/live/fn-live.js.map +1 -0
- package/dist/live/index.d.ts +4 -0
- package/dist/live/index.d.ts.map +1 -0
- package/dist/live/index.js +5 -0
- package/dist/live/index.js.map +1 -0
- package/dist/live/ioredis.d.ts +29 -0
- package/dist/live/ioredis.d.ts.map +1 -0
- package/dist/live/ioredis.js +49 -0
- package/dist/live/ioredis.js.map +1 -0
- package/dist/live/memory.d.ts +10 -0
- package/dist/live/memory.d.ts.map +1 -0
- package/dist/live/memory.js +45 -0
- package/dist/live/memory.js.map +1 -0
- package/dist/live/pub-sub.d.ts +211 -0
- package/dist/live/pub-sub.d.ts.map +1 -0
- package/dist/live/pub-sub.js +529 -0
- package/dist/live/pub-sub.js.map +1 -0
- package/dist/live/redis-bun.d.ts +26 -0
- package/dist/live/redis-bun.d.ts.map +1 -0
- package/dist/live/redis-bun.js +59 -0
- package/dist/live/redis-bun.js.map +1 -0
- package/dist/live/redis.d.ts +45 -0
- package/dist/live/redis.d.ts.map +1 -0
- package/dist/live/redis.js +214 -0
- package/dist/live/redis.js.map +1 -0
- package/dist/live/transport.d.ts +35 -0
- package/dist/live/transport.d.ts.map +1 -0
- package/dist/live/transport.js +2 -0
- package/dist/live/transport.js.map +1 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mastra.d.ts +44 -0
- package/dist/mastra.d.ts.map +1 -0
- package/dist/mastra.js +78 -0
- package/dist/mastra.js.map +1 -0
- package/dist/mcp.d.ts +39 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +38 -0
- package/dist/mcp.js.map +1 -0
- package/dist/meta.d.ts +35 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/meta.js +30 -0
- package/dist/meta.js.map +1 -0
- package/dist/otel.d.ts +66 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +63 -0
- package/dist/otel.js.map +1 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +14 -0
- package/dist/router.js.map +1 -0
- package/dist/stream.d.ts +13 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +44 -0
- package/dist/stream.js.map +1 -0
- package/dist/types.d.ts +196 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/package.json +157 -0
- package/src/bound-call.ts +50 -0
- package/src/client.ts +82 -0
- package/src/create-fn.ts +461 -0
- package/src/expected-client-error.ts +23 -0
- package/src/expo.ts +127 -0
- package/src/hono.ts +247 -0
- package/src/index.ts +45 -0
- package/src/json-schema.ts +82 -0
- package/src/live/codec.ts +31 -0
- package/src/live/fn-live.ts +470 -0
- package/src/live/index.ts +31 -0
- package/src/live/ioredis.ts +74 -0
- package/src/live/memory.ts +50 -0
- package/src/live/pub-sub.ts +828 -0
- package/src/live/redis-bun.ts +76 -0
- package/src/live/redis.ts +255 -0
- package/src/live/transport.ts +41 -0
- package/src/logger.ts +28 -0
- package/src/mastra.ts +140 -0
- package/src/mcp.ts +73 -0
- package/src/meta.ts +72 -0
- package/src/otel.ts +151 -0
- package/src/router.ts +15 -0
- package/src/stream.ts +54 -0
- package/src/types.ts +389 -0
package/src/hono.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { SmartCoercionPlugin } from '@orpc/json-schema'
|
|
2
|
+
import { OpenAPIHandler } from '@orpc/openapi/fetch'
|
|
3
|
+
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
|
|
4
|
+
import {
|
|
5
|
+
type AnyRouter,
|
|
6
|
+
type InferRouterInitialContext,
|
|
7
|
+
onError as orpcOnError
|
|
8
|
+
} from '@orpc/server'
|
|
9
|
+
import { RPCHandler } from '@orpc/server/fetch'
|
|
10
|
+
import { BatchHandlerPlugin } from '@orpc/server/plugins'
|
|
11
|
+
import type { StandardHandlerPlugin } from '@orpc/server/standard'
|
|
12
|
+
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
|
|
13
|
+
import type { Context as HonoContext, Env, Hono } from 'hono'
|
|
14
|
+
import type { StatusCode } from 'hono/utils/http-status'
|
|
15
|
+
import { isExpectedClientError } from './expected-client-error.js'
|
|
16
|
+
import type { ProcedureFilter } from './mcp.js'
|
|
17
|
+
|
|
18
|
+
/** Per-request timings in ms; `fn()` adds `handler_ms`. Sent as Server-Timing. */
|
|
19
|
+
export type RequestTiming = Record<string, number>
|
|
20
|
+
|
|
21
|
+
/** What every request starts with; the default context when `context` is omitted. */
|
|
22
|
+
export type BaseContext = { headers: Headers; timing: RequestTiming }
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds the router's initial context from the request. Return a `Response`
|
|
26
|
+
* to answer (e.g. 401) instead.
|
|
27
|
+
*/
|
|
28
|
+
export type ContextFactory<E extends Env, R extends AnyRouter> = (
|
|
29
|
+
c: HonoContext<E>,
|
|
30
|
+
base: BaseContext
|
|
31
|
+
) =>
|
|
32
|
+
| InferRouterInitialContext<R>
|
|
33
|
+
| Response
|
|
34
|
+
| Promise<InferRouterInitialContext<R> | Response>
|
|
35
|
+
|
|
36
|
+
/** `context` is optional only when the base context already satisfies the router. */
|
|
37
|
+
type ContextOption<E extends Env, R extends AnyRouter> =
|
|
38
|
+
BaseContext extends InferRouterInitialContext<R>
|
|
39
|
+
? { context?: ContextFactory<E, R> }
|
|
40
|
+
: { context: ContextFactory<E, R> }
|
|
41
|
+
|
|
42
|
+
type Path = `/${string}`
|
|
43
|
+
// biome-ignore lint/suspicious/noExplicitAny: plugins are context-agnostic here
|
|
44
|
+
type Plugin = StandardHandlerPlugin<any>
|
|
45
|
+
|
|
46
|
+
export type MountOrpcOptions<
|
|
47
|
+
E extends Env,
|
|
48
|
+
R extends AnyRouter
|
|
49
|
+
> = ContextOption<E, R> & {
|
|
50
|
+
router: R
|
|
51
|
+
/** Mount the RPC handler (oRPC's `RPCLink`) here. Omit to skip it. */
|
|
52
|
+
rpcPrefix?: Path
|
|
53
|
+
/** Mount the OpenAPI (REST) handler and its Scalar docs here. */
|
|
54
|
+
openapi?: {
|
|
55
|
+
prefix: Path
|
|
56
|
+
info?: { title: string; version: string; description?: string }
|
|
57
|
+
/** Docs page, relative to `prefix` (default '/'); the spec is at `/spec.json`. */
|
|
58
|
+
docsPath?: Path
|
|
59
|
+
/** Only expose matching procedures (also filters the spec). */
|
|
60
|
+
filter?: ProcedureFilter
|
|
61
|
+
/** Turn JSON strings back into `Date`s etc. for `z.date()` inputs. */
|
|
62
|
+
smartCoercion?: boolean
|
|
63
|
+
/** Extra spec fields, e.g. `components.securitySchemes` and `security`. */
|
|
64
|
+
spec?: Record<string, unknown>
|
|
65
|
+
plugins?: Plugin[]
|
|
66
|
+
}
|
|
67
|
+
/** Collapse parallel RPC calls into one HTTP request. Default: true (25 calls). */
|
|
68
|
+
batch?: boolean | { maxSize: number }
|
|
69
|
+
/** Add a Server-Timing header from the request timing. Default: true */
|
|
70
|
+
serverTiming?: boolean
|
|
71
|
+
/** SSE keep-alive comments every N ms (default 15000), or false. */
|
|
72
|
+
sseKeepAlive?: boolean | number
|
|
73
|
+
/** Extra RPC plugins (e.g. `CORSPlugin`). */
|
|
74
|
+
plugins?: Plugin[]
|
|
75
|
+
/** Rewrite request headers before they reach the context (see `normalizeExpoOrigin`). */
|
|
76
|
+
normalizeHeaders?: (headers: Headers) => Headers
|
|
77
|
+
/** Time spent before the handler ran, reported as `queue`. */
|
|
78
|
+
queueMs?: (request: Request) => number | undefined
|
|
79
|
+
/**
|
|
80
|
+
* Unexpected procedure errors (oRPC turns thrown errors into responses, so
|
|
81
|
+
* Hono's `onError` never sees them). Expected client errors are skipped.
|
|
82
|
+
*/
|
|
83
|
+
onError?: (error: unknown, request: { url: string; method: string }) => void
|
|
84
|
+
/** Default: 4xx `ORPCError` or `AbortError`. */
|
|
85
|
+
isExpectedError?: (error: unknown) => boolean
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Expo native sends `expo-origin` but not `origin`; copy it so cookie-based
|
|
90
|
+
* auth (e.g. Better Auth) sees the origin.
|
|
91
|
+
*/
|
|
92
|
+
export function normalizeExpoOrigin(raw: Headers): Headers {
|
|
93
|
+
const expo = raw.get('expo-origin')
|
|
94
|
+
if (!expo || raw.get('origin')) return raw
|
|
95
|
+
const headers = new Headers(raw)
|
|
96
|
+
headers.set('origin', expo)
|
|
97
|
+
return headers
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function formatServerTiming(
|
|
101
|
+
totalMs: number,
|
|
102
|
+
timing: Record<string, number | undefined>
|
|
103
|
+
): string {
|
|
104
|
+
const parts = [`total;dur=${totalMs.toFixed(2)}`]
|
|
105
|
+
for (const [key, value] of Object.entries(timing)) {
|
|
106
|
+
if (value !== undefined) {
|
|
107
|
+
parts.push(`${key.replace(/_ms$/, '')};dur=${value.toFixed(2)}`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return parts.join(', ')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Response headers plus Server-Timing. For SSE, also disable proxy buffering
|
|
115
|
+
* so keep-alive comments reach the edge (empty `:` lines are otherwise easy
|
|
116
|
+
* for nginx/Fastly to hold until an idle kill). A copy: some responses (e.g.
|
|
117
|
+
* redirects) have immutable headers.
|
|
118
|
+
*/
|
|
119
|
+
function finishHeaders(
|
|
120
|
+
response: Response,
|
|
121
|
+
serverTiming: string | undefined
|
|
122
|
+
): Headers {
|
|
123
|
+
const headers = new Headers(response.headers)
|
|
124
|
+
if (serverTiming) {
|
|
125
|
+
headers.set('Server-Timing', serverTiming)
|
|
126
|
+
headers.append('Access-Control-Expose-Headers', 'Server-Timing')
|
|
127
|
+
}
|
|
128
|
+
if ((headers.get('content-type') ?? '').includes('text/event-stream')) {
|
|
129
|
+
headers.set('Cache-Control', 'no-cache')
|
|
130
|
+
headers.set('X-Accel-Buffering', 'no')
|
|
131
|
+
}
|
|
132
|
+
return headers
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Mount oRPC's RPC and OpenAPI handlers on a Hono app. */
|
|
136
|
+
export function mountOrpc<E extends Env, R extends AnyRouter>(
|
|
137
|
+
app: Hono<E>,
|
|
138
|
+
options: MountOrpcOptions<E, R>
|
|
139
|
+
): void {
|
|
140
|
+
const {
|
|
141
|
+
router,
|
|
142
|
+
batch = true,
|
|
143
|
+
serverTiming = true,
|
|
144
|
+
sseKeepAlive = true
|
|
145
|
+
} = options
|
|
146
|
+
const isExpectedError = options.isExpectedError ?? isExpectedClientError
|
|
147
|
+
const keepAlive =
|
|
148
|
+
sseKeepAlive === false
|
|
149
|
+
? { eventIteratorKeepAliveEnabled: false }
|
|
150
|
+
: {
|
|
151
|
+
eventIteratorKeepAliveEnabled: true,
|
|
152
|
+
// Explicit keep-alive under proxy idle timers (~30s); oRPC's default is 5s.
|
|
153
|
+
eventIteratorKeepAliveInterval:
|
|
154
|
+
sseKeepAlive === true ? 15_000 : sseKeepAlive
|
|
155
|
+
}
|
|
156
|
+
const report = (
|
|
157
|
+
error: unknown,
|
|
158
|
+
{ request }: { request: { url: URL; method: string } }
|
|
159
|
+
) => {
|
|
160
|
+
if (isExpectedError(error)) return
|
|
161
|
+
options.onError?.(error, {
|
|
162
|
+
url: request.url.pathname,
|
|
163
|
+
method: request.method
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const mount = (prefix: Path, handler: Pick<RPCHandler<object>, 'handle'>) => {
|
|
168
|
+
app.use(`${prefix}/*`, async (c, next) => {
|
|
169
|
+
const timing: RequestTiming = {}
|
|
170
|
+
const queueMs = options.queueMs?.(c.req.raw)
|
|
171
|
+
if (queueMs !== undefined)
|
|
172
|
+
timing.queue_ms = Math.round(queueMs * 100) / 100
|
|
173
|
+
const started = performance.now()
|
|
174
|
+
const headers = options.normalizeHeaders
|
|
175
|
+
? options.normalizeHeaders(c.req.raw.headers)
|
|
176
|
+
: c.req.raw.headers
|
|
177
|
+
const context = options.context
|
|
178
|
+
? await options.context(c, { headers, timing })
|
|
179
|
+
: { headers, timing }
|
|
180
|
+
if (context instanceof Response) return context
|
|
181
|
+
const { matched, response } = await handler.handle(c.req.raw, {
|
|
182
|
+
prefix,
|
|
183
|
+
context: context as object
|
|
184
|
+
})
|
|
185
|
+
if (!matched) return next()
|
|
186
|
+
const timed = serverTiming
|
|
187
|
+
? formatServerTiming(performance.now() - started, timing)
|
|
188
|
+
: undefined
|
|
189
|
+
return c.newResponse(response.body, {
|
|
190
|
+
// oRPC only produces valid HTTP status codes.
|
|
191
|
+
status: response.status as StatusCode,
|
|
192
|
+
headers: finishHeaders(response, timed)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (options.rpcPrefix) {
|
|
198
|
+
const plugins: Plugin[] = [...(options.plugins ?? [])]
|
|
199
|
+
if (batch) {
|
|
200
|
+
plugins.push(
|
|
201
|
+
new BatchHandlerPlugin({
|
|
202
|
+
maxSize: batch === true ? 25 : batch.maxSize
|
|
203
|
+
})
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
mount(
|
|
207
|
+
options.rpcPrefix,
|
|
208
|
+
new RPCHandler(router, {
|
|
209
|
+
...keepAlive,
|
|
210
|
+
plugins,
|
|
211
|
+
interceptors: options.onError ? [orpcOnError(report)] : []
|
|
212
|
+
})
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (options.openapi) {
|
|
217
|
+
const { openapi } = options
|
|
218
|
+
const filter = openapi.filter ? { filter: openapi.filter } : {}
|
|
219
|
+
const converters = [new ZodToJsonSchemaConverter()]
|
|
220
|
+
const plugins: Plugin[] = [...(openapi.plugins ?? [])]
|
|
221
|
+
if (openapi.smartCoercion) {
|
|
222
|
+
plugins.push(new SmartCoercionPlugin({ schemaConverters: converters }))
|
|
223
|
+
}
|
|
224
|
+
plugins.push(
|
|
225
|
+
new OpenAPIReferencePlugin({
|
|
226
|
+
docsProvider: 'scalar',
|
|
227
|
+
...(openapi.docsPath ? { docsPath: openapi.docsPath } : {}),
|
|
228
|
+
schemaConverters: converters,
|
|
229
|
+
specGenerateOptions: {
|
|
230
|
+
info: openapi.info ?? { title: 'API', version: '0.0.0' },
|
|
231
|
+
servers: [{ url: openapi.prefix }],
|
|
232
|
+
...filter,
|
|
233
|
+
...openapi.spec
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
)
|
|
237
|
+
mount(
|
|
238
|
+
openapi.prefix,
|
|
239
|
+
new OpenAPIHandler(router, {
|
|
240
|
+
...keepAlive,
|
|
241
|
+
...filter,
|
|
242
|
+
plugins,
|
|
243
|
+
interceptors: options.onError ? [orpcOnError(report)] : []
|
|
244
|
+
})
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export { type BoundCall, createBoundCall } from './bound-call.js'
|
|
2
|
+
export {
|
|
3
|
+
type CreateFnOptions,
|
|
4
|
+
createFn,
|
|
5
|
+
type FnFactory
|
|
6
|
+
} from './create-fn.js'
|
|
7
|
+
export { isExpectedClientError } from './expected-client-error.js'
|
|
8
|
+
export { createBoundedEventQueue } from './live/pub-sub.js'
|
|
9
|
+
export {
|
|
10
|
+
createDefaultLogger,
|
|
11
|
+
type FnLogger,
|
|
12
|
+
type LogAttributes
|
|
13
|
+
} from './logger.js'
|
|
14
|
+
export { FN_META_KEY, type FnMeta, readFnMeta } from './meta.js'
|
|
15
|
+
export type {
|
|
16
|
+
AttributeValue,
|
|
17
|
+
OtelApiLike,
|
|
18
|
+
SpanLike,
|
|
19
|
+
SpanOf,
|
|
20
|
+
Tracing
|
|
21
|
+
} from './otel.js'
|
|
22
|
+
export { createRouter } from './router.js'
|
|
23
|
+
export type {
|
|
24
|
+
AnyFnContext,
|
|
25
|
+
BuilderLike,
|
|
26
|
+
CurrentContextOf,
|
|
27
|
+
Fn,
|
|
28
|
+
FnCompletedEvent,
|
|
29
|
+
FnContext,
|
|
30
|
+
FnDefinition,
|
|
31
|
+
FnProcedure,
|
|
32
|
+
FnRouteOptions,
|
|
33
|
+
Guard,
|
|
34
|
+
GuardOptions,
|
|
35
|
+
GuardParams,
|
|
36
|
+
HandlerParams,
|
|
37
|
+
InitialContextOf,
|
|
38
|
+
ProcedureContext,
|
|
39
|
+
ProcedureInput,
|
|
40
|
+
ProcedureKey,
|
|
41
|
+
ProcedureOption,
|
|
42
|
+
ProcedureOutput,
|
|
43
|
+
ReservedOptionKey,
|
|
44
|
+
RouteConfig
|
|
45
|
+
} from './types.js'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { AnySchema } from '@orpc/contract'
|
|
2
|
+
import { JsonSchemaCoercer } from '@orpc/json-schema'
|
|
3
|
+
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
|
|
4
|
+
|
|
5
|
+
export type JsonSchema = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A Standard Schema with JSON Schema attached, as MCP SDKs and Mastra expect.
|
|
9
|
+
* `TIn` is what is validated and what `validate` returns.
|
|
10
|
+
*/
|
|
11
|
+
export type ToolSchema<TIn = unknown, TOut = TIn> = {
|
|
12
|
+
'~standard': {
|
|
13
|
+
version: 1
|
|
14
|
+
vendor: string
|
|
15
|
+
validate: (
|
|
16
|
+
value: unknown
|
|
17
|
+
) =>
|
|
18
|
+
| { value: TOut; issues?: undefined }
|
|
19
|
+
| { issues: ReadonlyArray<{ message: string }> }
|
|
20
|
+
| Promise<
|
|
21
|
+
| { value: TOut; issues?: undefined }
|
|
22
|
+
| { issues: ReadonlyArray<{ message: string }> }
|
|
23
|
+
>
|
|
24
|
+
types?: { input: TIn; output: TOut }
|
|
25
|
+
jsonSchema: {
|
|
26
|
+
input: (options?: unknown) => JsonSchema
|
|
27
|
+
output: (options?: unknown) => JsonSchema
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const converter = new ZodToJsonSchemaConverter()
|
|
33
|
+
const coercer = new JsonSchemaCoercer()
|
|
34
|
+
|
|
35
|
+
function toJsonSchema(
|
|
36
|
+
schema: AnySchema,
|
|
37
|
+
strategy: 'input' | 'output'
|
|
38
|
+
): JsonSchema {
|
|
39
|
+
return converter.convert(schema, { strategy })[1] as JsonSchema
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tool input that validates like the procedure but returns the RAW input for
|
|
44
|
+
* the procedure's own input schema to parse. Transforms run in both passes
|
|
45
|
+
* but never on their own output, which handing over the parsed value would do. Zod's JSON Schema export rejects types like
|
|
46
|
+
* `z.date()`; oRPC's converter gives the same JSON Schema as the OpenAPI docs
|
|
47
|
+
* (dates become string/date-time), and clients send those dates as strings,
|
|
48
|
+
* so values are coerced back before validating.
|
|
49
|
+
*/
|
|
50
|
+
export function rawInputSchema<TIn>(schema: AnySchema): ToolSchema<TIn> {
|
|
51
|
+
const jsonSchema = toJsonSchema(schema, 'input')
|
|
52
|
+
return {
|
|
53
|
+
'~standard': {
|
|
54
|
+
version: 1,
|
|
55
|
+
vendor: 'orpc-fn',
|
|
56
|
+
validate: (value) => {
|
|
57
|
+
const coerced = coercer.coerce(jsonSchema, value) as TIn
|
|
58
|
+
const finish = (
|
|
59
|
+
result: Awaited<ReturnType<AnySchema['~standard']['validate']>>
|
|
60
|
+
) => (result.issues ? { issues: result.issues } : { value: coerced })
|
|
61
|
+
const result = schema['~standard'].validate(coerced)
|
|
62
|
+
return result instanceof Promise ? result.then(finish) : finish(result)
|
|
63
|
+
},
|
|
64
|
+
jsonSchema: { input: () => jsonSchema, output: () => jsonSchema }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Tool output the procedure already validated: described, passed through. */
|
|
70
|
+
export function passThroughOutputSchema<TOut>(
|
|
71
|
+
schema: AnySchema
|
|
72
|
+
): ToolSchema<TOut> {
|
|
73
|
+
const jsonSchema = toJsonSchema(schema, 'output')
|
|
74
|
+
return {
|
|
75
|
+
'~standard': {
|
|
76
|
+
version: 1,
|
|
77
|
+
vendor: 'orpc-fn',
|
|
78
|
+
validate: (value) => ({ value: value as TOut }),
|
|
79
|
+
jsonSchema: { input: () => jsonSchema, output: () => jsonSchema }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { StandardRPCJsonSerializer } from '@orpc/client/standard'
|
|
2
|
+
|
|
3
|
+
const serializer = new StandardRPCJsonSerializer()
|
|
4
|
+
const MARKER = 'orpc-fn'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wire format for pub/sub payloads: oRPC's own JSON serializer, so values
|
|
8
|
+
* plain JSON loses (`Date`, `Map`, `Set`, `BigInt`, `URL`, `undefined`) arrive
|
|
9
|
+
* intact. Payloads without the envelope are read as plain JSON, so a
|
|
10
|
+
* publisher on the old format keeps working during a rolling deploy.
|
|
11
|
+
*/
|
|
12
|
+
export function encodePayload(value: unknown): string {
|
|
13
|
+
const [json, meta, maps, blobs] = serializer.serialize(value)
|
|
14
|
+
if (maps.length > 0 || blobs.length > 0) {
|
|
15
|
+
throw new TypeError('orpc-fn: pub/sub events cannot contain files or blobs')
|
|
16
|
+
}
|
|
17
|
+
return JSON.stringify({ [MARKER]: 1, json, meta })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function decodePayload(payload: string): unknown {
|
|
21
|
+
const parsed: unknown = JSON.parse(payload)
|
|
22
|
+
if (
|
|
23
|
+
typeof parsed === 'object' &&
|
|
24
|
+
parsed !== null &&
|
|
25
|
+
(parsed as Record<string, unknown>)[MARKER] === 1
|
|
26
|
+
) {
|
|
27
|
+
const { json, meta } = parsed as { json: unknown; meta: [] }
|
|
28
|
+
return serializer.deserialize(json, meta)
|
|
29
|
+
}
|
|
30
|
+
return parsed
|
|
31
|
+
}
|