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/otel.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAsyncIteratorObject,
|
|
3
|
+
onStreamEnd,
|
|
4
|
+
type StreamScope
|
|
5
|
+
} from './stream.js'
|
|
6
|
+
|
|
7
|
+
/** Attribute values accepted by OpenTelemetry spans. */
|
|
8
|
+
export type AttributeValue = string | number | boolean
|
|
9
|
+
|
|
10
|
+
/** The part of an OpenTelemetry `Span` this package uses. */
|
|
11
|
+
export interface SpanLike {
|
|
12
|
+
setAttribute(key: string, value: AttributeValue): unknown
|
|
13
|
+
setStatus(status: { code: number; message?: string }): unknown
|
|
14
|
+
recordException(exception: Error): unknown
|
|
15
|
+
end(): unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type SpanOptionsLike = {
|
|
19
|
+
kind?: number
|
|
20
|
+
attributes?: Record<string, AttributeValue>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TracerLike<TSpan> {
|
|
24
|
+
startActiveSpan<F extends (span: TSpan) => unknown>(
|
|
25
|
+
name: string,
|
|
26
|
+
options: SpanOptionsLike,
|
|
27
|
+
fn: F
|
|
28
|
+
): ReturnType<F>
|
|
29
|
+
startSpan(name: string, options?: SpanOptionsLike): TSpan
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The part of `@opentelemetry/api` this package uses. Pass the module itself:
|
|
34
|
+
* `import * as otel from '@opentelemetry/api'` then `createFn({ otel })`.
|
|
35
|
+
* Without it no spans are created and handlers receive `span: undefined`.
|
|
36
|
+
*/
|
|
37
|
+
export interface OtelApiLike<TSpan extends SpanLike = SpanLike> {
|
|
38
|
+
trace: { getTracer(name: string): TracerLike<TSpan> }
|
|
39
|
+
SpanStatusCode: { OK: number; ERROR: number }
|
|
40
|
+
SpanKind: {
|
|
41
|
+
INTERNAL: number
|
|
42
|
+
SERVER: number
|
|
43
|
+
PRODUCER: number
|
|
44
|
+
}
|
|
45
|
+
/** Restores a span's context for each step of a stream it returned. */
|
|
46
|
+
context: {
|
|
47
|
+
active(): unknown
|
|
48
|
+
with<T>(context: never, fn: () => T): T
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The span type of an OpenTelemetry API, or `SpanLike` when absent. */
|
|
53
|
+
export type SpanOf<TOtel> =
|
|
54
|
+
TOtel extends OtelApiLike<infer TSpan> ? TSpan : SpanLike
|
|
55
|
+
|
|
56
|
+
/** Called once when a span's work is done, before the span ends. */
|
|
57
|
+
export type OnSettled = (
|
|
58
|
+
span: SpanLike | undefined,
|
|
59
|
+
...error: [] | [unknown]
|
|
60
|
+
) => void
|
|
61
|
+
|
|
62
|
+
export type Tracing = {
|
|
63
|
+
/**
|
|
64
|
+
* Run `fn` inside an active span: OK when it resolves, ERROR when it throws,
|
|
65
|
+
* ended either way. When `fn` resolves to a stream, the span stays open -
|
|
66
|
+
* and active for each step of the stream - until the stream ends.
|
|
67
|
+
* `onSettled` runs once at that point, before the span ends. Without
|
|
68
|
+
* OpenTelemetry `span` is undefined and `onSettled` still runs.
|
|
69
|
+
*/
|
|
70
|
+
inSpan<T>(
|
|
71
|
+
name: string,
|
|
72
|
+
kind: 'INTERNAL' | 'SERVER' | 'PRODUCER',
|
|
73
|
+
fn: (span: SpanLike | undefined) => Promise<T>,
|
|
74
|
+
onSettled?: OnSettled
|
|
75
|
+
): Promise<T>
|
|
76
|
+
/** Start a span that the caller ends with `end` (long-lived streams). */
|
|
77
|
+
startSpan(
|
|
78
|
+
name: string,
|
|
79
|
+
kind: 'SERVER',
|
|
80
|
+
attributes: Record<string, AttributeValue>
|
|
81
|
+
): SpanLike | undefined
|
|
82
|
+
/** Set OK, or ERROR with `error`, and end the span. */
|
|
83
|
+
end(span: SpanLike | undefined, error?: unknown): void
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createTracing(otel: OtelApiLike | undefined): Tracing {
|
|
87
|
+
const tracer = otel?.trace.getTracer('orpc-fn')
|
|
88
|
+
const end: Tracing['end'] = (span, ...error) => {
|
|
89
|
+
if (!span || !otel) return
|
|
90
|
+
if (error.length === 0) {
|
|
91
|
+
span.setStatus({ code: otel.SpanStatusCode.OK })
|
|
92
|
+
} else {
|
|
93
|
+
span.setStatus({
|
|
94
|
+
code: otel.SpanStatusCode.ERROR,
|
|
95
|
+
message: errorMessageOf(error[0])
|
|
96
|
+
})
|
|
97
|
+
if (error[0] instanceof Error) span.recordException(error[0])
|
|
98
|
+
}
|
|
99
|
+
span.end()
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
inSpan(name, kind, fn, onSettled) {
|
|
103
|
+
const settle = (span: SpanLike | undefined, ...error: [] | [unknown]) => {
|
|
104
|
+
try {
|
|
105
|
+
onSettled?.(span, ...error)
|
|
106
|
+
} finally {
|
|
107
|
+
end(span, ...error)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const run = async (
|
|
111
|
+
span: SpanLike | undefined,
|
|
112
|
+
scope?: StreamScope
|
|
113
|
+
): Promise<Awaited<ReturnType<typeof fn>>> => {
|
|
114
|
+
let result: Awaited<ReturnType<typeof fn>>
|
|
115
|
+
try {
|
|
116
|
+
result = await fn(span)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
settle(span, error)
|
|
119
|
+
throw error
|
|
120
|
+
}
|
|
121
|
+
if (isAsyncIteratorObject(result)) {
|
|
122
|
+
return onStreamEnd(
|
|
123
|
+
result,
|
|
124
|
+
(...error) => settle(span, ...error),
|
|
125
|
+
scope
|
|
126
|
+
) as typeof result
|
|
127
|
+
}
|
|
128
|
+
settle(span)
|
|
129
|
+
return result
|
|
130
|
+
}
|
|
131
|
+
if (!tracer || !otel) return run(undefined)
|
|
132
|
+
return tracer.startActiveSpan(
|
|
133
|
+
name,
|
|
134
|
+
{ kind: otel.SpanKind[kind] },
|
|
135
|
+
(span) => {
|
|
136
|
+
const active = otel.context.active()
|
|
137
|
+
return run(span, (step) => otel.context.with(active as never, step))
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
},
|
|
141
|
+
startSpan(name, kind, attributes) {
|
|
142
|
+
if (!tracer || !otel) return undefined
|
|
143
|
+
return tracer.startSpan(name, { kind: otel.SpanKind[kind], attributes })
|
|
144
|
+
},
|
|
145
|
+
end
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function errorMessageOf(error: unknown): string {
|
|
150
|
+
return error instanceof Error ? error.message : String(error)
|
|
151
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AnyRouter } from '@orpc/server'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a router object from routes or nested routers.
|
|
5
|
+
* Simply returns the routes as a const object for type inference.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const items = createRouter({ getItem, updateItem })
|
|
10
|
+
* export const api = createRouter({ metabase, items })
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export function createRouter<T extends AnyRouter>(routes: T): Readonly<T> {
|
|
14
|
+
return routes
|
|
15
|
+
}
|
package/src/stream.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** A value oRPC streams to the client (an async generator or iterator). */
|
|
2
|
+
export function isAsyncIteratorObject(
|
|
3
|
+
value: unknown
|
|
4
|
+
): value is AsyncIterator<unknown> & AsyncIterable<unknown> {
|
|
5
|
+
return (
|
|
6
|
+
typeof value === 'object' &&
|
|
7
|
+
value !== null &&
|
|
8
|
+
typeof (value as AsyncIterable<unknown>)[Symbol.asyncIterator] ===
|
|
9
|
+
'function' &&
|
|
10
|
+
typeof (value as AsyncIterator<unknown>).next === 'function'
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Runs one step of a stream inside some context (e.g. its tracing span). */
|
|
15
|
+
export type StreamScope = <T>(step: () => T) => T
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a stream so `finish` runs once when it ends: after the last item,
|
|
19
|
+
* when it throws (with the error), or when the consumer stops early with
|
|
20
|
+
* `return()` - also before the first `next()`, which a plain async generator
|
|
21
|
+
* wrapper would miss. With `scope`, every `next`/`return`/`throw` runs inside
|
|
22
|
+
* it: a generator's body runs on those calls, long after it was created.
|
|
23
|
+
*/
|
|
24
|
+
export function onStreamEnd<T>(
|
|
25
|
+
stream: AsyncIterator<T> & AsyncIterable<T>,
|
|
26
|
+
finish: (...error: [] | [unknown]) => void,
|
|
27
|
+
scope: StreamScope = (step) => step()
|
|
28
|
+
): AsyncIterator<T> & AsyncIterable<T> {
|
|
29
|
+
let finished = false
|
|
30
|
+
const done = (...error: [] | [unknown]) => {
|
|
31
|
+
if (finished) return
|
|
32
|
+
finished = true
|
|
33
|
+
finish(...error)
|
|
34
|
+
}
|
|
35
|
+
const settle = async (step: () => Promise<IteratorResult<T>> | undefined) => {
|
|
36
|
+
try {
|
|
37
|
+
const result = await step()
|
|
38
|
+
if (!result || result.done) done()
|
|
39
|
+
return result ?? { done: true as const, value: undefined }
|
|
40
|
+
} catch (error) {
|
|
41
|
+
done(error)
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const wrapped: AsyncIterator<T> & AsyncIterable<T> = {
|
|
46
|
+
next: (...args) => settle(() => scope(() => stream.next(...args))),
|
|
47
|
+
return: (value) =>
|
|
48
|
+
settle(() => scope(() => stream.return?.(value))).finally(() => done()),
|
|
49
|
+
throw: (error) =>
|
|
50
|
+
settle(() => scope(() => stream.throw?.(error) ?? Promise.reject(error))),
|
|
51
|
+
[Symbol.asyncIterator]: () => wrapped
|
|
52
|
+
}
|
|
53
|
+
return wrapped
|
|
54
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnySchema,
|
|
3
|
+
ErrorMap,
|
|
4
|
+
InferSchemaInput,
|
|
5
|
+
InferSchemaOutput,
|
|
6
|
+
Meta,
|
|
7
|
+
Schema
|
|
8
|
+
} from '@orpc/contract'
|
|
9
|
+
import type {
|
|
10
|
+
AnyProcedure,
|
|
11
|
+
DecoratedProcedure,
|
|
12
|
+
Procedure,
|
|
13
|
+
Route
|
|
14
|
+
} from '@orpc/server'
|
|
15
|
+
import type { ZodType, z } from 'zod'
|
|
16
|
+
import type { BoundCall } from './bound-call.js'
|
|
17
|
+
import type { FnLogger } from './logger.js'
|
|
18
|
+
import type { SpanLike } from './otel.js'
|
|
19
|
+
|
|
20
|
+
export type MaybePromise<T> = T | Promise<T>
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* An oRPC builder without input or output schema: `os`, `os.$context<T>()`,
|
|
24
|
+
* `os.use(...)`. A schema set on the builder is rejected by `createFn`; set it
|
|
25
|
+
* on the route instead.
|
|
26
|
+
*/
|
|
27
|
+
/* biome-ignore-start lint/suspicious/noExplicitAny: matches every builder's generic methods */
|
|
28
|
+
export type BuilderLike = {
|
|
29
|
+
route(route: any): unknown
|
|
30
|
+
meta(meta: any): unknown
|
|
31
|
+
input(schema: any): unknown
|
|
32
|
+
output(schema: any): unknown
|
|
33
|
+
handler(handler: any): Procedure<any, any, any, any, any, any>
|
|
34
|
+
}
|
|
35
|
+
/* biome-ignore-end lint/suspicious/noExplicitAny: matches every builder's generic methods */
|
|
36
|
+
|
|
37
|
+
/** Input a procedure is called with (before its input schema runs). */
|
|
38
|
+
export type ProcedureInput<T> =
|
|
39
|
+
T extends Procedure<
|
|
40
|
+
infer _TInitial,
|
|
41
|
+
infer _TCurrent,
|
|
42
|
+
infer TInputSchema extends AnySchema,
|
|
43
|
+
infer _TOut,
|
|
44
|
+
infer _TErrors,
|
|
45
|
+
infer _TMeta
|
|
46
|
+
>
|
|
47
|
+
? InferSchemaInput<TInputSchema>
|
|
48
|
+
: unknown
|
|
49
|
+
|
|
50
|
+
/** What a procedure resolves to (after its output schema runs). */
|
|
51
|
+
export type ProcedureOutput<T> =
|
|
52
|
+
T extends Procedure<
|
|
53
|
+
infer _TInitial,
|
|
54
|
+
infer _TCurrent,
|
|
55
|
+
infer _TIn,
|
|
56
|
+
infer TOutputSchema extends AnySchema,
|
|
57
|
+
infer _TErrors,
|
|
58
|
+
infer _TMeta
|
|
59
|
+
>
|
|
60
|
+
? InferSchemaOutput<TOutputSchema>
|
|
61
|
+
: unknown
|
|
62
|
+
|
|
63
|
+
/** Context a procedure must be called with. */
|
|
64
|
+
export type ProcedureContext<T> =
|
|
65
|
+
T extends Procedure<
|
|
66
|
+
infer TContext,
|
|
67
|
+
infer _TCurrent,
|
|
68
|
+
infer _TIn,
|
|
69
|
+
infer _TOut,
|
|
70
|
+
infer _TErrors,
|
|
71
|
+
infer _TMeta
|
|
72
|
+
>
|
|
73
|
+
? TContext
|
|
74
|
+
: never
|
|
75
|
+
|
|
76
|
+
/** `Omit` that keeps each member of a union (handler params narrow by discriminant). */
|
|
77
|
+
export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown
|
|
78
|
+
? Omit<T, K>
|
|
79
|
+
: never
|
|
80
|
+
|
|
81
|
+
/** `fn()` options passed to oRPC's `.route()`; `RouteConfig` is derived from it. */
|
|
82
|
+
export const ROUTE_OPTION_KEYS = [
|
|
83
|
+
'path',
|
|
84
|
+
'method',
|
|
85
|
+
'summary',
|
|
86
|
+
'description',
|
|
87
|
+
'deprecated',
|
|
88
|
+
'successStatus',
|
|
89
|
+
'successDescription',
|
|
90
|
+
'inputStructure',
|
|
91
|
+
'outputStructure',
|
|
92
|
+
'tags'
|
|
93
|
+
] as const satisfies readonly (keyof Route)[]
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `fn()` option names owned by the route itself. Guard and meta keys must not
|
|
97
|
+
* reuse them: such an option would be read as the route option and the guard
|
|
98
|
+
* would never run.
|
|
99
|
+
*/
|
|
100
|
+
export const RESERVED_OPTION_KEYS = [
|
|
101
|
+
...ROUTE_OPTION_KEYS,
|
|
102
|
+
'name',
|
|
103
|
+
'procedure',
|
|
104
|
+
'input',
|
|
105
|
+
'output',
|
|
106
|
+
'handler',
|
|
107
|
+
'live',
|
|
108
|
+
'operationId'
|
|
109
|
+
] as const
|
|
110
|
+
|
|
111
|
+
export type ReservedOptionKey = (typeof RESERVED_OPTION_KEYS)[number]
|
|
112
|
+
|
|
113
|
+
/** @internal Builds one route from a `procedures` builder; shared by `fn` and `createPubSub`. */
|
|
114
|
+
export type BuildProcedure = (options: {
|
|
115
|
+
name: string
|
|
116
|
+
procedure: string | undefined
|
|
117
|
+
route: Record<string, unknown>
|
|
118
|
+
meta: Record<string, unknown>
|
|
119
|
+
input?: AnySchema | undefined
|
|
120
|
+
output?: AnySchema | undefined
|
|
121
|
+
handler: (options: {
|
|
122
|
+
input: unknown
|
|
123
|
+
context: Record<string, unknown>
|
|
124
|
+
signal?: AbortSignal
|
|
125
|
+
}) => unknown
|
|
126
|
+
}) => AnyProcedure
|
|
127
|
+
|
|
128
|
+
/** Turns every key of `T` found in `K` into a readable type error. */
|
|
129
|
+
export type RejectKeys<T, K extends PropertyKey, TMessage extends string> = {
|
|
130
|
+
[P in Extract<keyof T, K>]: TMessage
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Handler extras must have known keys: an index signature would claim every
|
|
135
|
+
* handler param. Intersected into `createFn`'s options to reject it.
|
|
136
|
+
*/
|
|
137
|
+
export type FiniteExtras<T> = string extends keyof T
|
|
138
|
+
? { extras: 'orpc-fn: extras need known keys, not an index signature' }
|
|
139
|
+
: number extends keyof T
|
|
140
|
+
? { extras: 'orpc-fn: extras need known keys, not an index signature' }
|
|
141
|
+
: unknown
|
|
142
|
+
|
|
143
|
+
type ProcedureOf<TBuilder> = TBuilder extends {
|
|
144
|
+
handler(handler: never): infer TProcedure
|
|
145
|
+
}
|
|
146
|
+
? TProcedure
|
|
147
|
+
: never
|
|
148
|
+
|
|
149
|
+
/** Context a route built from this builder is called with. */
|
|
150
|
+
export type InitialContextOf<TBuilder> = ProcedureContext<ProcedureOf<TBuilder>>
|
|
151
|
+
|
|
152
|
+
/** Context the handler sees, after the builder's middleware. */
|
|
153
|
+
export type CurrentContextOf<TBuilder> =
|
|
154
|
+
ProcedureOf<TBuilder> extends Procedure<
|
|
155
|
+
infer _TInitial,
|
|
156
|
+
infer TContext,
|
|
157
|
+
infer _TIn,
|
|
158
|
+
infer _TOut,
|
|
159
|
+
infer _TErrors,
|
|
160
|
+
infer _TMeta
|
|
161
|
+
>
|
|
162
|
+
? TContext
|
|
163
|
+
: never
|
|
164
|
+
|
|
165
|
+
type ErrorMapOf<TBuilder> =
|
|
166
|
+
ProcedureOf<TBuilder> extends {
|
|
167
|
+
'~orpc': { errorMap: infer T extends ErrorMap }
|
|
168
|
+
}
|
|
169
|
+
? T
|
|
170
|
+
: ErrorMap
|
|
171
|
+
type MetaOf<TBuilder> =
|
|
172
|
+
ProcedureOf<TBuilder> extends { '~orpc': { meta: infer T extends Meta } }
|
|
173
|
+
? T
|
|
174
|
+
: Meta
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Everything `createFn` inferred from its options. Route types read from it;
|
|
178
|
+
* apps rarely spell it out.
|
|
179
|
+
*/
|
|
180
|
+
export type FnDefinition = {
|
|
181
|
+
procedures: Record<string, BuilderLike>
|
|
182
|
+
default: string
|
|
183
|
+
extras: object
|
|
184
|
+
guards: object
|
|
185
|
+
meta: object
|
|
186
|
+
tag: string
|
|
187
|
+
span: SpanLike
|
|
188
|
+
logger: FnLogger
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type ProcedureKey<TDef extends FnDefinition> = keyof TDef['procedures'] &
|
|
192
|
+
string
|
|
193
|
+
|
|
194
|
+
/** Context the handler of a `procedure: TKey` route receives. */
|
|
195
|
+
export type FnContext<
|
|
196
|
+
TDef extends FnDefinition,
|
|
197
|
+
TKey extends ProcedureKey<TDef>
|
|
198
|
+
> = CurrentContextOf<TDef['procedures'][TKey]>
|
|
199
|
+
|
|
200
|
+
/** Union of every procedure's handler context: what guards and extras see. */
|
|
201
|
+
export type AnyFnContext<TDef extends FnDefinition> = {
|
|
202
|
+
[K in ProcedureKey<TDef>]: FnContext<TDef, K>
|
|
203
|
+
}[ProcedureKey<TDef>]
|
|
204
|
+
|
|
205
|
+
export type GuardParams<TContext, TMeta> = {
|
|
206
|
+
context: TContext
|
|
207
|
+
input: unknown
|
|
208
|
+
/** Route name (operationId). */
|
|
209
|
+
name: string
|
|
210
|
+
/** Procedures key the route was built from. */
|
|
211
|
+
procedure: string
|
|
212
|
+
meta: Partial<TMeta>
|
|
213
|
+
signal: AbortSignal | undefined
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A check run before the handler. Its first parameter becomes an option on
|
|
218
|
+
* `fn({...})`; the guard runs only on routes that set it. Throw (usually an
|
|
219
|
+
* `ORPCError`) to refuse the call.
|
|
220
|
+
*/
|
|
221
|
+
export type Guard<TValue, TParams> = (value: TValue, params: TParams) => unknown
|
|
222
|
+
|
|
223
|
+
export type GuardOptions<TGuards> = {
|
|
224
|
+
[K in keyof TGuards]?: TGuards[K] extends (
|
|
225
|
+
value: infer TValue,
|
|
226
|
+
params: never
|
|
227
|
+
) => unknown
|
|
228
|
+
? TValue
|
|
229
|
+
: never
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export type HandlerParams<
|
|
233
|
+
TDef extends FnDefinition,
|
|
234
|
+
TInput,
|
|
235
|
+
TKey extends ProcedureKey<TDef>
|
|
236
|
+
> = DistributiveOmit<
|
|
237
|
+
TDef['extras'],
|
|
238
|
+
'input' | 'context' | 'call' | 'signal' | 'span' | 'logger'
|
|
239
|
+
> & {
|
|
240
|
+
input: TInput
|
|
241
|
+
context: FnContext<TDef, TKey>
|
|
242
|
+
/** Calls another procedure with this context and signal, in a child span. */
|
|
243
|
+
call: BoundCall<FnContext<TDef, TKey>>
|
|
244
|
+
/** Cancellation is inherited by nested calls; pass to interruptible I/O. */
|
|
245
|
+
signal: AbortSignal | undefined
|
|
246
|
+
/** Active OpenTelemetry span, undefined when `createFn({ otel })` is not set. */
|
|
247
|
+
span: TDef['span'] | undefined
|
|
248
|
+
logger: TDef['logger']
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Route options shared by every `fn()` overload. */
|
|
252
|
+
export type RouteConfig<TTag extends string = string> = Pick<
|
|
253
|
+
Route,
|
|
254
|
+
Exclude<(typeof ROUTE_OPTION_KEYS)[number], 'tags'>
|
|
255
|
+
> & {
|
|
256
|
+
/** Required. Becomes the OpenAPI operationId and the span name (e.g. 'user.create'). */
|
|
257
|
+
name: string
|
|
258
|
+
tags?: TTag[]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export type FnRouteOptions<
|
|
262
|
+
TDef extends FnDefinition,
|
|
263
|
+
TKey extends ProcedureKey<TDef>
|
|
264
|
+
> = RouteConfig<TDef['tag']> &
|
|
265
|
+
GuardOptions<TDef['guards']> &
|
|
266
|
+
TDef['meta'] &
|
|
267
|
+
ProcedureOption<TDef, TKey>
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Which `createFn({ procedures })` builder a route uses. Optional when
|
|
271
|
+
* `createFn` has a `default`; required on every route when it has none.
|
|
272
|
+
*/
|
|
273
|
+
export type ProcedureOption<
|
|
274
|
+
TDef extends FnDefinition,
|
|
275
|
+
TKey extends ProcedureKey<TDef>
|
|
276
|
+
> = [TDef['default']] extends [never]
|
|
277
|
+
? { procedure: TKey }
|
|
278
|
+
: { procedure?: TKey }
|
|
279
|
+
|
|
280
|
+
export type FnProcedure<
|
|
281
|
+
TDef extends FnDefinition,
|
|
282
|
+
TKey extends ProcedureKey<TDef>,
|
|
283
|
+
TInputSchema extends Schema<unknown, unknown>,
|
|
284
|
+
TOutputSchema extends Schema<unknown, unknown>
|
|
285
|
+
> = DecoratedProcedure<
|
|
286
|
+
InitialContextOf<TDef['procedures'][TKey]>,
|
|
287
|
+
FnContext<TDef, TKey>,
|
|
288
|
+
TInputSchema,
|
|
289
|
+
TOutputSchema,
|
|
290
|
+
ErrorMapOf<TDef['procedures'][TKey]>,
|
|
291
|
+
MetaOf<TDef['procedures'][TKey]>
|
|
292
|
+
>
|
|
293
|
+
|
|
294
|
+
type InputOf<T> = [T] extends [ZodType] ? z.output<T> : unknown
|
|
295
|
+
type ReturnOf<TOut, TReturn> = [TOut] extends [ZodType]
|
|
296
|
+
? z.input<TOut>
|
|
297
|
+
: TReturn
|
|
298
|
+
|
|
299
|
+
/** `fn()`: four overloads for input/output schema × inferred. */
|
|
300
|
+
export interface Fn<TDef extends FnDefinition> {
|
|
301
|
+
// 1. With input + output schema
|
|
302
|
+
<
|
|
303
|
+
TInput extends ZodType,
|
|
304
|
+
TOut extends ZodType,
|
|
305
|
+
TKey extends ProcedureKey<TDef> = TDef['default']
|
|
306
|
+
>(
|
|
307
|
+
options: {
|
|
308
|
+
input: TInput
|
|
309
|
+
output: TOut
|
|
310
|
+
// The output schema parses what the handler returns: its input type.
|
|
311
|
+
handler: (
|
|
312
|
+
params: HandlerParams<TDef, z.output<TInput>, NoInfer<TKey>>
|
|
313
|
+
) => MaybePromise<z.input<TOut>>
|
|
314
|
+
} & FnRouteOptions<TDef, TKey>
|
|
315
|
+
): FnProcedure<TDef, TKey, TInput, TOut>
|
|
316
|
+
|
|
317
|
+
// 2. With input, inferred output
|
|
318
|
+
<
|
|
319
|
+
TInput extends ZodType,
|
|
320
|
+
TReturn,
|
|
321
|
+
TKey extends ProcedureKey<TDef> = TDef['default']
|
|
322
|
+
>(
|
|
323
|
+
options: {
|
|
324
|
+
input: TInput
|
|
325
|
+
output?: undefined
|
|
326
|
+
handler: (
|
|
327
|
+
params: HandlerParams<TDef, z.output<TInput>, NoInfer<TKey>>
|
|
328
|
+
) => MaybePromise<TReturn>
|
|
329
|
+
} & FnRouteOptions<TDef, TKey>
|
|
330
|
+
): FnProcedure<TDef, TKey, TInput, Schema<TReturn, TReturn>>
|
|
331
|
+
|
|
332
|
+
// 3. No input, with output schema
|
|
333
|
+
<TOut extends ZodType, TKey extends ProcedureKey<TDef> = TDef['default']>(
|
|
334
|
+
options: {
|
|
335
|
+
input?: undefined
|
|
336
|
+
output: TOut
|
|
337
|
+
handler: (
|
|
338
|
+
params: HandlerParams<TDef, unknown, NoInfer<TKey>>
|
|
339
|
+
) => MaybePromise<z.input<TOut>>
|
|
340
|
+
} & FnRouteOptions<TDef, TKey>
|
|
341
|
+
): FnProcedure<TDef, TKey, Schema<unknown, unknown>, TOut>
|
|
342
|
+
|
|
343
|
+
// 4. No input, inferred output
|
|
344
|
+
<TReturn, TKey extends ProcedureKey<TDef> = TDef['default']>(
|
|
345
|
+
options: {
|
|
346
|
+
input?: undefined
|
|
347
|
+
output?: undefined
|
|
348
|
+
handler: (
|
|
349
|
+
params: HandlerParams<TDef, unknown, NoInfer<TKey>>
|
|
350
|
+
) => MaybePromise<TReturn>
|
|
351
|
+
} & FnRouteOptions<TDef, TKey>
|
|
352
|
+
): FnProcedure<TDef, TKey, Schema<unknown, unknown>, Schema<TReturn, TReturn>>
|
|
353
|
+
|
|
354
|
+
// 5. Never matches valid code first; it only words the type errors, which
|
|
355
|
+
// TypeScript reports against the last overload.
|
|
356
|
+
<
|
|
357
|
+
TReturn,
|
|
358
|
+
TInput extends ZodType | undefined = undefined,
|
|
359
|
+
TOut extends ZodType | undefined = undefined,
|
|
360
|
+
TKey extends ProcedureKey<TDef> = TDef['default']
|
|
361
|
+
>(
|
|
362
|
+
options: {
|
|
363
|
+
input?: TInput
|
|
364
|
+
output?: TOut
|
|
365
|
+
// The output schema parses what the handler returns: its input type.
|
|
366
|
+
handler: (
|
|
367
|
+
params: HandlerParams<TDef, InputOf<TInput>, NoInfer<TKey>>
|
|
368
|
+
) => MaybePromise<ReturnOf<TOut, TReturn>>
|
|
369
|
+
} & FnRouteOptions<TDef, TKey>
|
|
370
|
+
): FnProcedure<
|
|
371
|
+
TDef,
|
|
372
|
+
TKey,
|
|
373
|
+
[TInput] extends [ZodType] ? TInput : Schema<unknown, unknown>,
|
|
374
|
+
[TOut] extends [ZodType] ? TOut : Schema<TReturn, TReturn>
|
|
375
|
+
>
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** What `fn.completed` reports, and what `onCompleted` receives. */
|
|
379
|
+
export type FnCompletedEvent<TContext = unknown, TSpan = SpanLike> = {
|
|
380
|
+
name: string
|
|
381
|
+
procedure: string
|
|
382
|
+
durationMs: number
|
|
383
|
+
success: boolean
|
|
384
|
+
error: unknown
|
|
385
|
+
input: unknown
|
|
386
|
+
context: TContext
|
|
387
|
+
span: TSpan | undefined
|
|
388
|
+
meta: Record<string, unknown>
|
|
389
|
+
}
|